From ed5b83662d00a69e917024ec69195ea4bece0e17 Mon Sep 17 00:00:00 2001 From: MTG2000 Date: Mon, 16 May 2022 18:32:09 +0300 Subject: [PATCH] feat: built initial version of Vote button component --- .../VoteButton/VoteButton.stories.tsx | 22 +++ src/Components/VoteButton/VoteButton.tsx | 154 ++++++++++++++++++ src/Components/VoteButton/styles.module.css | 115 +++++++++++++ .../VoteButton/VoteButton.stories.tsx | 2 +- .../ProjectPage/VoteButton/VoteButton.tsx | 22 +-- ...style.css => vote-button-style.module.css} | 8 +- 6 files changed, 307 insertions(+), 16 deletions(-) create mode 100644 src/Components/VoteButton/VoteButton.stories.tsx create mode 100644 src/Components/VoteButton/VoteButton.tsx create mode 100644 src/Components/VoteButton/styles.module.css rename src/features/Projects/pages/ProjectPage/VoteButton/{vote-button.style.css => vote-button-style.module.css} (94%) diff --git a/src/Components/VoteButton/VoteButton.stories.tsx b/src/Components/VoteButton/VoteButton.stories.tsx new file mode 100644 index 0000000..2ca2224 --- /dev/null +++ b/src/Components/VoteButton/VoteButton.stories.tsx @@ -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; + +const Template: ComponentStory = (args) => ; + + +export const Default = Template.bind({}); +Default.args = { + initVotes: 540, + onVote: () => { } +} \ No newline at end of file diff --git a/src/Components/VoteButton/VoteButton.tsx b/src/Components/VoteButton/VoteButton.tsx new file mode 100644 index 0000000..aae357e --- /dev/null +++ b/src/Components/VoteButton/VoteButton.tsx @@ -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, 'children'> + +export default function VoteButton({ initVotes, onVote = () => { }, ...props }: Props) { + const [voteCnt, setVoteCnt] = useState(0) + const voteCntRef = useRef(0); + const [sparks, setSparks] = useState([]); + const [wasActive, setWasActive] = useState(false); + const [incrementsCount, setIncrementsCount] = useState(0); + const incrementsCountRef = useRef(0); + const [increments, setIncrements] = useState>([]) + + 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 ( + + + ) +} diff --git a/src/Components/VoteButton/styles.module.css b/src/Components/VoteButton/styles.module.css new file mode 100644 index 0000000..1e54a88 --- /dev/null +++ b/src/Components/VoteButton/styles.module.css @@ -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; + } +} diff --git a/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.stories.tsx b/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.stories.tsx index 19465b0..4961f7d 100644 --- a/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.stories.tsx +++ b/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.stories.tsx @@ -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 diff --git a/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.tsx b/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.tsx index c43603f..c73df42 100644 --- a/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.tsx +++ b/src/features/Projects/pages/ProjectPage/VoteButton/VoteButton.tsx @@ -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 !!! {voteCnt}
{sparks.map(spark =>