import { MdLocalFireDepartment } from 'react-icons/md' import Button from 'src/Components/Button/Button' import { useAppSelector, usePressHolder, useResizeListener } from 'src/utils/hooks' import { ComponentProps, SyntheticEvent, useRef, useState } from 'react' import styles from './styles.module.scss' import { random, randomItem, numberFormatter } from 'src/utils/helperFunctions' import { useDebouncedCallback, useMountEffect, useThrottledCallback } from '@react-hookz/web' import { UnionToObjectKeys } from 'src/utils/types/utils' import { Portal } from '../Portal/Portal' import { ThreeDots } from 'react-loader-spinner' import { AnimatePresence, motion } from 'framer-motion' interface Particle { id: string, offsetX: number, offsetY: number, color: string animation: 'fly-spark-1' | 'fly-spark-2', animationSpeed: 1 | 2 | 3, scale: number } type Props = { votes: number, onVote?: (amount: number, config: Partial<{ onSetteled: () => void; onError: () => void; onSuccess: () => void; }>) => void, fillType?: 'leftRight' | 'upDown' | "background" | 'radial', direction?: 'horizontal' | 'vertical' disableCounter?: boolean disableShake?: boolean hideVotesCoun?: boolean dense?: boolean size?: 'sm' | 'md' resetCounterOnRelease?: boolean } & Omit, 'children'> const btnPadding: UnionToObjectKeys = { horizontal: { sm: '', md: '', } as UnionToObjectKeys, vertical: { sm: 'p-8', md: '', } as UnionToObjectKeys } type BtnState = 'ready' | 'voting' | 'loading' | "success" | "fail"; export default function VoteButton({ votes, onVote = () => { }, fillType = 'leftRight', direction = 'horizontal', disableCounter = false, disableShake = true, hideVotesCoun = false, dense = false, resetCounterOnRelease = true, ...props }: Props) { const [voteCnt, setVoteCnt] = useState(0) const voteCntRef = useRef(0); const btnContainerRef = useRef(null!!) const [btnShakeClass, setBtnShakeClass] = useState('') const [sparks, setSparks] = useState([]); const [incrementsCount, setIncrementsCount] = useState(0); const totalIncrementsCountRef = useRef(0) const currentIncrementsCountRef = useRef(0); const [increments, setIncrements] = useState>([]); const [btnPosition, setBtnPosition] = useState<{ top: number, left: number, width: number, height: number }>(); const [btnState, setBtnState] = useState('ready'); const doVote = useDebouncedCallback(() => { setBtnState('loading'); const amount = voteCntRef.current; onVote(amount, { onSuccess: () => { setBtnState("success"); spawnSparks(10); }, onError: () => setBtnState('fail'), onSetteled: () => { setVoteCnt(v => v - amount); setTimeout(() => { setBtnState("ready") if (resetCounterOnRelease) { setIncrementsCount(0); totalIncrementsCountRef.current = 0; currentIncrementsCountRef.current = 0; } voteCntRef.current = 0; }, 2000); } }); }, [], 1500); const spawnSparks = (cnt = 5) => { const newSparks = Array(cnt).fill(0).map((_, idx) => ({ id: (Math.random() + 1).toString(), offsetX: random(-10, 99), offsetY: random(10, 90), animation: randomItem(styles.fly_spark_1, styles.fly_spark_1), animationSpeed: randomItem(1, 1.5, 2), color: `hsl(0deg 86% ${random(50, 63)}%)`, scale: random(1, 1.5) })) // if on mobile screen, reduce number of sparks particles to 60% setSparks(oldSparks => [...oldSparks, ...newSparks]) setTimeout(() => { setSparks(s => { return s.filter(spark => !newSparks.some(newSpark => newSpark.id === spark.id)) }) }, 2 * 1000) } const clickIncrement = () => { if (!disableShake) setBtnShakeClass(s => s === styles.clicked_2 ? styles.clicked_1 : styles.clicked_2) const _incStep = Math.ceil((currentIncrementsCountRef.current + 1) / 5); currentIncrementsCountRef.current += 1; totalIncrementsCountRef.current += 1; setIncrementsCount(v => totalIncrementsCountRef.current); if (!disableCounter) setIncrements(v => { const genId = Math.random().toString(); setTimeout(() => { setIncrements(v => v.filter(e => e.id !== genId)) }, 500) return [...v, { id: genId, value: _incStep }] }); setVoteCnt(s => { const newValue = s + _incStep; voteCntRef.current = newValue; return newValue; }) // Each time the button make 5 increments, spawn some flames if (totalIncrementsCountRef.current && totalIncrementsCountRef.current % 5 === 0) spawnSparks(5); doVote(); } const onHold = useThrottledCallback(clickIncrement, [], 150) const { onPressDown, onPressUp } = usePressHolder(onHold, 200); const handlePressDown = () => { if (btnState !== 'ready' && btnState !== 'voting') return; setBtnState('voting'); onPressDown(); } const handlePressUp = (event?: SyntheticEvent) => { if (btnState !== 'voting') return; if (event?.preventDefault) event.preventDefault(); onPressUp(); onHold(); } useMountEffect(() => { const bodyRect = document.body.getBoundingClientRect(); const btnRect = btnContainerRef.current.getBoundingClientRect() setBtnPosition({ top: btnRect.top - bodyRect.top, left: btnRect.left - bodyRect.left, width: btnRect.width, height: btnRect.height }); }) useResizeListener(() => { const bodyRect = document.body.getBoundingClientRect(); const btnRect = btnContainerRef.current.getBoundingClientRect() setBtnPosition({ top: btnRect.top - bodyRect.top, left: btnRect.left - bodyRect.left, width: btnRect.width, height: btnRect.height }); }, { debounce: 300 }) return ( ) }