feat: built initial version of Vote button component

This commit is contained in:
MTG2000
2022-05-16 18:32:09 +03:00
parent 3964c086e6
commit ed5b83662d
6 changed files with 307 additions and 16 deletions

View File

@@ -0,0 +1,22 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { centerDecorator } from 'src/utils/storybook/decorators';
import VoteButton from './VoteButton';
export default {
title: 'Shared/Vote Button',
component: VoteButton,
decorators: [
centerDecorator
]
} as ComponentMeta<typeof VoteButton>;
const Template: ComponentStory<typeof VoteButton> = (args) => <VoteButton {...args} />;
export const Default = Template.bind({});
Default.args = {
initVotes: 540,
onVote: () => { }
}

View File

@@ -0,0 +1,154 @@
import { MdLocalFireDepartment } from 'react-icons/md'
import Button from 'src/Components/Button/Button'
import { useAppSelector, usePressHolder } from 'src/utils/hooks'
import _throttle from 'lodash.throttle'
import { ComponentProps, useRef, useState } from 'react'
import styles from './styles.module.css'
import { random, randomItem } from 'src/utils/helperFunctions'
import { useDebouncedCallback, useThrottledCallback } from '@react-hookz/web'
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 = {
initVotes: number
onVote: (Vote: number) => void
} & Omit<ComponentProps<typeof Button>, 'children'>
export default function VoteButton({ initVotes, onVote = () => { }, ...props }: Props) {
const [voteCnt, setVoteCnt] = useState(0)
const voteCntRef = useRef(0);
const [sparks, setSparks] = useState<Particle[]>([]);
const [wasActive, setWasActive] = useState(false);
const [incrementsCount, setIncrementsCount] = useState(0);
const incrementsCountRef = useRef(0);
const [increments, setIncrements] = useState<Array<{ id: string, value: number }>>([])
const isMobileScreen = useAppSelector(s => s.ui.isMobileScreen);
const doVote = useDebouncedCallback(() => {
onVote(voteCntRef.current);
voteCntRef.current = 0;
console.log("VOTED");
}, [], 2000)
const clickIncrement = () => {
const _incStep = Math.ceil((incrementsCountRef.current + 1) / 5);
incrementsCountRef.current += 1;
setIncrementsCount(v => incrementsCountRef.current);
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;
})
if (incrementsCountRef.current && incrementsCountRef.current % 5 === 0) {
const newSparks = Array(5).fill(0).map((_, idx) => ({
id: (Math.random() + 1).toString(),
offsetX: random(-10, 99),
offsetY: random(40, 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.2, 1.8)
}))
// 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)
}
doVote();
}
const onHold = useThrottledCallback(clickIncrement, [], 150)
const { onPressDown, onPressUp } = usePressHolder(onHold, 100);
const handlePressDown = () => {
setWasActive(true);
onPressDown();
}
const handlePressUp = (event?: any) => {
if (!wasActive) return;
setWasActive(false);
if (event?.preventDefault) event.preventDefault();
onPressUp();
onHold();
}
return (
<Button
onMouseDown={handlePressDown}
onMouseUp={handlePressUp}
onMouseLeave={handlePressUp}
onTouchStart={handlePressDown}
onTouchEnd={handlePressUp}
size='md'
color='none'
className={`text-gray-600 !border-0 bg-gray-25 hover:bg-gray-50 relative noselect`}
{...props}
>
<div
className={styles.color_overlay}
style={{
"--increments": incrementsCount,
"--offset": `${(incrementsCount ? (incrementsCount % 5 === 0 ? 5 : incrementsCount % 5) : 0) * 20}%`
} as any}
>
</div>
<div className={`relative z-10 ${incrementsCount ? "text-red-600" : "text-gray-600"}`}>
<MdLocalFireDepartment className='' /><span className="align-middle"> {initVotes + voteCnt}</span>
</div>
{increments.map(increment => <span
key={increment.id}
className={styles.vote_counter}
>+{increment.value}</span>)}
{sparks.map(spark =>
<div
key={spark.id}
className={styles.spark}
style={{
"--offsetX": spark.offsetX,
"--offsetY": spark.offsetY,
"--animationSpeed": spark.animationSpeed,
"--scale": spark.scale,
"animationName": spark.animation,
"color": spark.color
} as any}
><MdLocalFireDepartment className='' /></div>)
}
</Button>
)
}

View File

@@ -0,0 +1,115 @@
.vote_button {
--scale: 0;
/* transition: background-color 1s; */
/* background-color: hsl(25, 100%, max(calc((95 - var(--scale) / 4) * 1%), 63%)); */
}
.vote_counter {
position: absolute;
left: 50%;
bottom: 100%;
color: #ff2727;
font-weight: bold;
font-size: 21px;
will-change: transform;
text-shadow: 0 0 4px #ff0707;
transform: translate(-50%, 0) scale(0.5);
animation: fly_value 0.5s 1 ease-out;
}
.color_overlay {
--increments: 0;
--offset: 0;
--bg-color: hsl(0deg 86% max(calc((93 - var(--increments) / 3) * 1%), 68%));
position: absolute;
border-radius: inherit;
inset: 0;
overflow: hidden;
}
.color_overlay::before {
content: "";
background: var(--bg-color);
width: 100%;
height: 100%;
position: absolute;
top: 0;
right: 100%;
transform: translateX(var(--offset));
}
@keyframes fly_value {
0% {
transform: translate(-50%, 0) scale(0.5);
opacity: 1;
}
66% {
transform: translate(-50%, -36px) scale(1.2);
opacity: 0.6;
}
100% {
transform: translate(-50%, -48px) scale(0.8);
opacity: 0;
}
}
.spark {
position: absolute;
bottom: calc(var(--offsetY) * 1%);
left: calc(var(--offsetX) * 1%);
transform: scale(var(--scale));
opacity: 0;
will-change: transform;
animation-name: fly-spark-1;
animation-duration: calc(var(--animationSpeed) * 1s);
animation-timing-function: linear;
animation-iteration-count: 1;
animation-fill-mode: forwards;
filter: drop-shadow(0 0 4px);
}
@keyframes fly_spark_1 {
0% {
transform: translate(0, 0) scale(var(--scale));
opacity: 1;
}
33% {
transform: translate(12px, -70px) scale(var(--scale));
}
66% {
transform: translate(0, -140px) scale(var(--scale));
opacity: 0.6;
}
100% {
transform: translate(6px, -200px) scale(var(--scale));
opacity: 0;
}
}
@keyframes fly_spark_2 {
0% {
transform: translate(0, 0) scale(var(--scale));
opacity: 1;
}
50% {
transform: translate(-10px, -80px) scale(var(--scale));
}
80% {
transform: translate(-4px, -140px) scale(var(--scale));
opacity: 0.6;
}
100% {
transform: translate(-6px, -160px) scale(var(--scale));
opacity: 0;
}
}

View File

@@ -4,7 +4,7 @@ import VoteButton from './VoteButton';
import { centerDecorator } from 'src/utils/storybook/decorators';
export default {
title: 'Projects/Project Page/Tip Button',
title: 'Projects/Project Page/Vote Button',
component: VoteButton,
decorators: [
centerDecorator

View File

@@ -3,7 +3,7 @@ import Button from 'src/Components/Button/Button'
import { useAppSelector, usePressHolder } from 'src/utils/hooks'
import _throttle from 'lodash.throttle'
import { ComponentProps, useRef, useState } from 'react'
import './vote-button.style.css'
import styles from './vote-button-style.module.css'
import { random, randomItem } from 'src/utils/helperFunctions'
@@ -40,7 +40,7 @@ export default function VoteButton({ onVote = () => { }, ...props }: Props) {
const newSpark = {
id: Math.random().toString(),
offsetX: random(1, 99),
animation: randomItem('fly-spark-1', 'fly-spark-2'),
animation: randomItem(styles.fly_spark_1, styles.fly_spark_1),
animationSpeed: randomItem(1, 1.5, 2),
color: randomItem('#ff6a00', '#ff7717', '#ff6217', '#ff8217', '#ff5717'),
scale: random(1.2, 2.2)
@@ -91,7 +91,7 @@ export default function VoteButton({ onVote = () => { }, ...props }: Props) {
onTouchEnd={handlePressUp}
size='md'
color='none'
className="vote-button border relative 100 my-16 noselect"
className={`${styles.vote_button} border relative 100 my-16 noselect`}
style={{
"--scale": voteCnt,
} as any}
@@ -100,48 +100,48 @@ export default function VoteButton({ onVote = () => { }, ...props }: Props) {
Hold To Vote !!! <MdLocalFireDepartment className='text-fire' />
<span
className='vote-counter'
className={styles.vote_counter}
>{voteCnt}</span>
<div
className='spark'
className={styles.spark}
style={{
"--offsetX": 23,
"--animationSpeed": 3,
"--scale": 1,
"animationIterationCount": 'infinite',
"animationName": 'fly-spark-1',
"animationName": styles.fly_spark_1,
"animationDelay": '1.1s',
color: '#ff6a00'
} as any}
><MdLocalFireDepartment className='' /></div>
<div
className='spark'
className={styles.spark}
style={{
"--offsetX": 50,
"--animationSpeed": 2.2,
"--scale": 1,
"animationIterationCount": 'infinite',
"animationName": 'fly-spark-2',
"animationName": styles.fly_spark_2,
"animationDelay": '0.4s',
color: '#ff6a00'
} as any}
><MdLocalFireDepartment className='' /></div>
<div
className='spark'
className={styles.spark}
style={{
"--offsetX": 70,
"--animationSpeed": 2.5,
"--scale": 1,
"animationIterationCount": 'infinite',
"animationName": 'fly-spark-1',
"animationName": styles.fly_spark_1,
color: '#ff6a00'
} as any}
><MdLocalFireDepartment className='' /></div>
{sparks.map(spark =>
<div
key={spark.id}
className='spark'
className={styles.spark}
style={{
"--offsetX": spark.offsetX,
"--animationSpeed": spark.animationSpeed,

View File

@@ -1,10 +1,10 @@
.vote-button {
.vote_button {
--scale: 0;
transition: background-color 1s;
background-color: hsl(25, 100%, max(calc((95 - var(--scale) / 4) * 1%), 63%));
}
.vote-counter {
.vote_counter {
position: absolute;
left: 50%;
bottom: 110%;
@@ -34,7 +34,7 @@
filter: drop-shadow(0 0 4px);
}
@keyframes fly-spark-1 {
@keyframes fly_spark_1 {
0% {
transform: translate(0, 0) scale(var(--scale));
opacity: 1;
@@ -55,7 +55,7 @@
}
}
@keyframes fly-spark-2 {
@keyframes fly_spark_2 {
0% {
transform: translate(0, 0) scale(var(--scale));
opacity: 1;