added confetti and rolling digits animation to voting
This commit is contained in:
parent
a8b29e562e
commit
ebf7ea65a6
|
|
@ -268,7 +268,7 @@ big {
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-module_slot__DpPgW {
|
.index-module_slot__DpPgW {
|
||||||
overflow: visible !important;
|
/* overflow: visible !important; */
|
||||||
/* height: auto !important; */
|
/* height: auto !important; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import ReactConfetti from 'react-confetti';
|
||||||
|
import { useWindowSize } from 'react-use';
|
||||||
|
|
||||||
|
const Confetti = () => {
|
||||||
|
const { width, height } = useWindowSize();
|
||||||
|
const colors = [
|
||||||
|
'linear-gradient(45deg, #5D5D72, #8589DE)',
|
||||||
|
'linear-gradient(45deg, #E1E0FF, #575992)',
|
||||||
|
'#8589DE',
|
||||||
|
'#575992',
|
||||||
|
'#E1E0FF',
|
||||||
|
'#FF3131',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed top-0 left-0 z-50">
|
||||||
|
<ReactConfetti
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
recycle={true}
|
||||||
|
numberOfPieces={200}
|
||||||
|
tweenDuration={10000}
|
||||||
|
run={true}
|
||||||
|
colors={colors}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Confetti;
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import SlotCounter from 'react-slot-counter';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
value: string | number;
|
||||||
|
startValueOnce?: boolean;
|
||||||
|
animateOnVisible?: boolean;
|
||||||
|
autoAnimationStart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RollingCounter = ({
|
||||||
|
value,
|
||||||
|
startValueOnce = false,
|
||||||
|
animateOnVisible = false,
|
||||||
|
autoAnimationStart = false,
|
||||||
|
}: IProps) => {
|
||||||
|
return (
|
||||||
|
<SlotCounter
|
||||||
|
value={value ? value : 0}
|
||||||
|
startValueOnce={startValueOnce}
|
||||||
|
animateOnVisible={animateOnVisible}
|
||||||
|
autoAnimationStart={autoAnimationStart}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RollingCounter;
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
|
||||||
|
|
||||||
interface RollingCounterProps {
|
|
||||||
numberString: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move constants outside component
|
|
||||||
const ROLLS = 2;
|
|
||||||
const DIGIT_HEIGHT = 104;
|
|
||||||
const EXTRA_NUMBERS_BEFORE = 2;
|
|
||||||
const EXTRA_NUMBERS_AFTER = 5;
|
|
||||||
const CONTAINER_HEIGHT = 180;
|
|
||||||
const INITIAL_OFFSET = (CONTAINER_HEIGHT - DIGIT_HEIGHT) / 2 - DIGIT_HEIGHT * 2;
|
|
||||||
|
|
||||||
// Memoize number generation function
|
|
||||||
const getNumbers = (targetValue: number) => {
|
|
||||||
const numbers = [];
|
|
||||||
|
|
||||||
// Add extra numbers before
|
|
||||||
for (let n = 10 - EXTRA_NUMBERS_BEFORE; n < 10; n++) {
|
|
||||||
numbers.push(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start with zeros
|
|
||||||
for (let n = 0; n <= targetValue; n++) {
|
|
||||||
numbers.push(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add complete rolls after the initial sequence
|
|
||||||
for (let i = 0; i < ROLLS; i++) {
|
|
||||||
for (let n = 0; n < 10; n++) {
|
|
||||||
numbers.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sequence to target
|
|
||||||
for (let n = 0; n <= targetValue; n++) {
|
|
||||||
numbers.push(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add extra numbers after target
|
|
||||||
for (let n = (targetValue + 1) % 10; n < ((targetValue + 1) % 10) + EXTRA_NUMBERS_AFTER; n++) {
|
|
||||||
numbers.push(n % 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return numbers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RollingDigit = ({
|
|
||||||
targetValue,
|
|
||||||
index,
|
|
||||||
onAnimationComplete,
|
|
||||||
isStopped,
|
|
||||||
showHyphen,
|
|
||||||
totalDigits,
|
|
||||||
isRollingBack,
|
|
||||||
}: {
|
|
||||||
targetValue: number;
|
|
||||||
index: number;
|
|
||||||
onAnimationComplete: () => void;
|
|
||||||
isStopped: boolean;
|
|
||||||
showHyphen: boolean;
|
|
||||||
totalDigits: number;
|
|
||||||
isRollingBack: boolean;
|
|
||||||
}) => {
|
|
||||||
const numbers = useMemo(() => getNumbers(targetValue), [targetValue]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="overflow-hidden h-[180px] w-[77px] relative">
|
|
||||||
<motion.div
|
|
||||||
initial={{ y: INITIAL_OFFSET }}
|
|
||||||
animate={{
|
|
||||||
y: isRollingBack
|
|
||||||
? INITIAL_OFFSET
|
|
||||||
: -(numbers.length - EXTRA_NUMBERS_AFTER - 3) * DIGIT_HEIGHT + INITIAL_OFFSET,
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: isRollingBack ? 2 : 2,
|
|
||||||
delay: isRollingBack ? index * 0.2 : (totalDigits - 1 - 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 && !isRollingBack && 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 [isStopped, setIsStopped] = useState<boolean[]>([]);
|
|
||||||
const [isRollingBack, setIsRollingBack] = useState(false);
|
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
||||||
const [currentNumbers, setCurrentNumbers] = useState<number[]>([]);
|
|
||||||
const prevNumberStringRef = useRef(numberString);
|
|
||||||
|
|
||||||
const { numbers, isInitialLoading } = useMemo(() => {
|
|
||||||
if (!numberString) {
|
|
||||||
return { numbers: [], isInitialLoading: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = numberString.split('-').flatMap((pair) => {
|
|
||||||
return pair.split('').map((char) => parseInt(char, 10));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
numbers: parsed,
|
|
||||||
isInitialLoading: false,
|
|
||||||
};
|
|
||||||
}, [numberString]);
|
|
||||||
|
|
||||||
// Initialize currentNumbers
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentNumbers.length && numbers.length) {
|
|
||||||
setCurrentNumbers(numbers);
|
|
||||||
}
|
|
||||||
}, [numbers, currentNumbers.length]);
|
|
||||||
|
|
||||||
// Handle number changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (prevNumberStringRef.current !== numberString && !isRollingBack) {
|
|
||||||
setIsTransitioning(true);
|
|
||||||
setIsRollingBack(true);
|
|
||||||
setIsStopped(new Array(numbers.length).fill(false));
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsRollingBack(false);
|
|
||||||
setCurrentNumbers(numbers);
|
|
||||||
prevNumberStringRef.current = numberString;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, [numberString, numbers, isRollingBack]);
|
|
||||||
|
|
||||||
const handleAnimationComplete = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (!isRollingBack) {
|
|
||||||
setIsStopped((prev) => {
|
|
||||||
const newState = [...prev];
|
|
||||||
newState[index] = true;
|
|
||||||
|
|
||||||
if (newState.every((stopped) => stopped)) {
|
|
||||||
setIsTransitioning(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isRollingBack],
|
|
||||||
);
|
|
||||||
|
|
||||||
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 max-w-[1132px] justify-center bg-lightPrimary text-white py-4 px-6 rounded-full">
|
|
||||||
{currentNumbers.map((num, index) => (
|
|
||||||
<RollingDigit
|
|
||||||
key={`${index}`} // Simplified key to prevent re-renders
|
|
||||||
targetValue={num}
|
|
||||||
index={index}
|
|
||||||
onAnimationComplete={() => handleAnimationComplete(index)}
|
|
||||||
isStopped={isStopped[index] && !isTransitioning}
|
|
||||||
showHyphen={(index + 1) % 2 === 0 && index !== numbers.length - 1}
|
|
||||||
totalDigits={numbers.length}
|
|
||||||
isRollingBack={isRollingBack}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RollingCounter;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { cn } from "@/lib/utils"; // Assuming `cn` is defined in `lib/utils`
|
|
||||||
|
|
||||||
const Confetti: React.FC = () => {
|
|
||||||
const [confetti, setConfetti] = useState<number[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const confettiArray = Array.from({ length: 300 }, (_, i) => i); // Generate 300 confetti pieces
|
|
||||||
setConfetti(confettiArray);
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setConfetti([]);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const randomColor = () => {
|
|
||||||
const colors = [
|
|
||||||
"linear-gradient(45deg, #5D5D72, #8589DE)",
|
|
||||||
"linear-gradient(45deg, #E1E0FF, #575992)",
|
|
||||||
"#8589DE",
|
|
||||||
"#575992",
|
|
||||||
"#E1E0FF",
|
|
||||||
];
|
|
||||||
return colors[Math.floor(Math.random() * colors.length)];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (confetti.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 overflow-hidden pointer-events-none z-50">
|
|
||||||
{confetti.map((_, i) => {
|
|
||||||
const randomSize = Math.random() * 10 + 8; // Size between 8px and 18px
|
|
||||||
const randomStartX = Math.random() * 100; // Start anywhere across the screen
|
|
||||||
const randomStartY = Math.random() < 0.5 ? -10 : 110; // Start above or below the screen
|
|
||||||
const randomDuration = Math.random() * 1 + 4; // Duration between 4s and 5s
|
|
||||||
const randomEndX = Math.random() * 200 - 100; // Drift horizontally (-100vw to +100vw)
|
|
||||||
const randomRotation = Math.random() * 360; // Start with a random rotation
|
|
||||||
const randomDelay = Math.random() * 0.5; // Delay between 0-0.5s
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={cn("absolute", "confetti-piece")}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
top: `${randomStartY}%`,
|
|
||||||
left: `${randomStartX}%`,
|
|
||||||
width: `${randomSize}px`,
|
|
||||||
height: `${randomSize}px`,
|
|
||||||
background: randomColor(),
|
|
||||||
animationDuration: `${randomDuration}s`,
|
|
||||||
animationDelay: `${randomDelay}s`,
|
|
||||||
transform: `rotate(${randomRotation}deg)`,
|
|
||||||
"--end-x": `${randomEndX}vw`,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Confetti;
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Image from 'next/image';
|
||||||
import placeholder from '@/public/person placeholder.svg';
|
import placeholder from '@/public/person placeholder.svg';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { motion, usePresence, Variant, Transition } from 'framer-motion';
|
import { motion, usePresence, Variant, Transition } from 'framer-motion';
|
||||||
|
import RollingCounter from 'react-slot-counter';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
number: number;
|
number: number;
|
||||||
|
|
@ -128,7 +129,13 @@ const ParticipantCard = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-[8px]">
|
<div className="flex flex-col items-end gap-[8px]">
|
||||||
<h4 className="text-[12px] sm:text-[48px] text-white leading-[100%] font-bold">
|
<h4 className="text-[12px] sm:text-[48px] text-white leading-[100%] font-bold">
|
||||||
{votes ? votes : 0}
|
{/* {votes ? votes : 0} */}
|
||||||
|
<RollingCounter
|
||||||
|
value={votes ? votes : 0}
|
||||||
|
startValueOnce={true}
|
||||||
|
animateOnVisible={false}
|
||||||
|
autoAnimationStart={false}
|
||||||
|
/>
|
||||||
</h4>
|
</h4>
|
||||||
<h4 className="text-[12px] sm:text-[24px] text-white leading-[100%] font-bold">
|
<h4 className="text-[12px] sm:text-[24px] text-white leading-[100%] font-bold">
|
||||||
ses
|
ses
|
||||||
|
|
@ -210,7 +217,13 @@ const ParticipantCard = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[4px] items-end">
|
<div className="flex flex-col gap-[4px] items-end">
|
||||||
<h4 className="text-[12px] sm:text-[32px] text-[#808080] font-bold leading-[100%] ">
|
<h4 className="text-[12px] sm:text-[32px] text-[#808080] font-bold leading-[100%] ">
|
||||||
{votes ? votes : 0}
|
{/* {votes ? votes : 0} */}
|
||||||
|
<RollingCounter
|
||||||
|
value={votes ? votes : 0}
|
||||||
|
startValueOnce={true}
|
||||||
|
animateOnVisible={false}
|
||||||
|
autoAnimationStart={false}
|
||||||
|
/>
|
||||||
</h4>
|
</h4>
|
||||||
<h4 className="text-[12px] sm:text-[16px] text-[#808080] font-bold leading-[100%] ">
|
<h4 className="text-[12px] sm:text-[16px] text-[#808080] font-bold leading-[100%] ">
|
||||||
ses
|
ses
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import Image from 'next/image';
|
||||||
import { useMediaQuery } from 'usehooks-ts';
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
import Countdown from './Countdown';
|
import Countdown from './Countdown';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { useWindowSize } from 'react-use';
|
||||||
|
import Confetti from '../common/Confetti';
|
||||||
|
|
||||||
interface IParams {
|
interface IParams {
|
||||||
vote_id?: string;
|
vote_id?: string;
|
||||||
|
|
@ -30,7 +31,7 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
||||||
const [data, setData] = useState<IAllVotes>();
|
const [data, setData] = useState<IAllVotes>();
|
||||||
const [participantsData, setParticipantsData] = useState<VotingItem[]>([]);
|
const [participantsData, setParticipantsData] = useState<VotingItem[]>([]);
|
||||||
const [voteStatus, setVoteStatus] = useState<string>();
|
const [voteStatus, setVoteStatus] = useState<string>();
|
||||||
const [eventStatus, setEventStatus] = useState<string>('Finished');
|
const [eventStatus, setEventStatus] = useState<string>('Not started');
|
||||||
const [manualClose, setManualClose] = useState(false); // Track manual closure
|
const [manualClose, setManualClose] = useState(false); // Track manual closure
|
||||||
|
|
||||||
const [winnersCount, setWinnersCount] = useState<number>(0);
|
const [winnersCount, setWinnersCount] = useState<number>(0);
|
||||||
|
|
@ -41,6 +42,7 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
const mobile = useMediaQuery('(max-width: 768px)');
|
const mobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
const { width, height } = useWindowSize();
|
||||||
|
|
||||||
const { setVoteDescription } = useContext(VoteContext).voteDescriptionContext;
|
const { setVoteDescription } = useContext(VoteContext).voteDescriptionContext;
|
||||||
|
|
||||||
|
|
@ -209,6 +211,8 @@ const ParticipantsList = ({ vote_id }: IParams) => {
|
||||||
<div className="flex flex-col gap-[20px] sm:gap-[40px] w-full items-center">
|
<div className="flex flex-col gap-[20px] sm:gap-[40px] w-full items-center">
|
||||||
{data.data.description ? <PageBage title={data.data.description} /> : null}
|
{data.data.description ? <PageBage title={data.data.description} /> : null}
|
||||||
|
|
||||||
|
{eventStatus === 'Finished' && <Confetti />}
|
||||||
|
|
||||||
{data.data.banner ? (
|
{data.data.banner ? (
|
||||||
<div className="relative w-full md:min-h-[150px] md:h-auto h-[100px] ">
|
<div className="relative w-full md:min-h-[150px] md:h-auto h-[100px] ">
|
||||||
{mobile ? (
|
{mobile ? (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue