mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-27 11:14:33 +01:00
feat: built initial version of Vote button component
This commit is contained in:
22
src/Components/VoteButton/VoteButton.stories.tsx
Normal file
22
src/Components/VoteButton/VoteButton.stories.tsx
Normal 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: () => { }
|
||||
}
|
||||
154
src/Components/VoteButton/VoteButton.tsx
Normal file
154
src/Components/VoteButton/VoteButton.tsx
Normal 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>
|
||||
|
||||
)
|
||||
}
|
||||
115
src/Components/VoteButton/styles.module.css
Normal file
115
src/Components/VoteButton/styles.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user