feat: Created an animated Tip button

This commit is contained in:
MTG2000
2022-01-16 20:51:24 +02:00
parent 44d1285681
commit ea20ea6784
24 changed files with 375 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect } from "react";
import Navbar from "src/Components/Navbar/Navbar";
import ExplorePage from "src/pages/ExplorePage";
import ModalsContainer from "src/Components/Modals/ModalsContainer/ModalsContainer";
@@ -23,10 +23,10 @@ function App() {
console.log("error:webln.enable()", err);
});
}
}, []);
}, [dispatch]);
useResizeListener(() => {
dispatch(setIsMobileScreen(document.body.clientWidth < 768));
// dispatch(setIsMobileScreen(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)));
}, [dispatch])

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Login_ExternalWalletCard from './Login_ExternalWalletCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Login/External Wallet Card',

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Login_NativeWalletCard from './Login_NativeWalletCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Login/Native Wallet Card',

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Login_ScanningWalletCard from './Login_ScanningWalletCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Login/Scanning Wallet Card',

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Login_SuccessCard from './Login_SuccessCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Login/Success Card',

View File

@@ -71,6 +71,7 @@ export default function ModalsContainer() {
<AnimatePresence>
{openModals.map((modal, idx) => {
const Child = ALL_MODALS[modal.Modal];
return (
<Modal key={idx} onClose={onClose} direction={direction} isPageModal={modal.props?.isPageModal}>
<Child onClose={onClose} direction={direction} isPageModal={modal.props?.isPageModal} {...modal.props} />

View File

@@ -0,0 +1,17 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import TipButton from './TipButton';
import { centerDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Shared/Tip Button',
component: TipButton,
decorators: [
centerDecorator
]
} as ComponentMeta<typeof TipButton>;
const Template: ComponentStory<typeof TipButton> = (args) => <TipButton onTip={() => { }} />;
export const Default = Template.bind({});

View File

@@ -0,0 +1,176 @@
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, useEffect, useState } from 'react'
import './tipbutton.style.css'
import { random, randomItem } from 'src/utils/helperFunctions'
interface Particle {
id: string,
offsetX: number,
color: '#ff6a00' | '#ff7717' | '#ff6217' | '#ff8217' | '#ff5717'
animation: 'fly-spark-1' | 'fly-spark-2',
animationSpeed: 1 | 2 | 3,
scale: number
}
type Props = {
onTip: (tip: number) => void
} & Omit<ComponentProps<typeof Button>, 'children'>
export default function TipButton({ onTip = () => { }, ...props }: Props) {
const [tipCnt, setTipCnt] = useState(0)
const [sparks, setSparks] = useState<Particle[]>([]);
const [wasActive, setWasActive] = useState(false);
const isMobileScreen = useAppSelector(s => s.theme.isMobileScreen)
// useEffect(() => {
// setInterval(() => {
// setTipCnt(s => s + 1)
// const newSpark = {
// id: Math.random().toString(),
// offsetX: random(1, 99),
// animation: randomItem('fly-spark-1', 'fly-spark-2'),
// animationSpeed: randomItem(1, 1.5, 2),
// color: randomItem('#ff6a00', '#ff7717', '#ff6217', '#ff8217', '#ff5717'),
// scale: random(1, 2)
// };
// setTimeout(() => {
// setSparks(s => {
// return s.filter(spark => spark.id !== newSpark.id)
// })
// }, newSpark.animationSpeed * 1000)
// setSparks(oldSparks => [...oldSparks, newSpark])
// }, 300);
// }, [])
const { onPressDown, onPressUp } = usePressHolder(_throttle(() => {
const incStep = (Math.ceil((tipCnt + 1) / 100) + 1) ** 2;
setTipCnt(s => s + incStep)
const newSpark = {
id: Math.random().toString(),
offsetX: random(1, 99),
animation: randomItem('fly-spark-1', 'fly-spark-2'),
animationSpeed: randomItem(1, 1.5, 2),
color: randomItem('#ff6a00', '#ff7717', '#ff6217', '#ff8217', '#ff5717'),
scale: random(1.2, 2.2)
};
// if on mobile screen, reduce number of sparks particles to 60%
if (!isMobileScreen || Math.random() > .4) {
setSparks(oldSparks => [...oldSparks, newSpark])
setTimeout(() => {
setSparks(s => {
return s.filter(spark => spark.id !== newSpark.id)
})
}, newSpark.animationSpeed * 1000)
}
}, 100), 100);
const handlePressDown = () => {
setWasActive(true);
onPressDown();
}
const handlePressUp = (event?: any) => {
if (!wasActive) return;
setWasActive(false);
if (event?.preventDefault) event.preventDefault();
onPressUp();
if (tipCnt === 0)
onTip(10);
else
setTimeout(() => {
setSparks([]);
onTip(tipCnt);
setTipCnt(0);
}, 1000)
}
return (
<Button
onMouseDown={handlePressDown}
onMouseUp={handlePressUp}
onMouseLeave={handlePressUp}
onTouchStart={handlePressDown}
onTouchEnd={handlePressUp}
size='md'
color='none'
className="tip-button border relative 100 my-16 noselect"
style={{
"--scale": tipCnt,
} as any}
{...props}
>
Hold To Tip !!! <MdLocalFireDepartment className='text-fire' />
<span
className='tip-counter'
>{tipCnt}</span>
<div
className='spark'
style={{
"--offsetX": 23,
"--animationSpeed": 3,
"--scale": 1,
"animationIterationCount": 'infinite',
"animationName": 'fly-spark-1',
"animationDelay": '1.1s',
color: '#ff6a00'
} as any}
><MdLocalFireDepartment className='' /></div>
<div
className='spark'
style={{
"--offsetX": 50,
"--animationSpeed": 2.2,
"--scale": 1,
"animationIterationCount": 'infinite',
"animationName": 'fly-spark-2',
"animationDelay": '0.4s',
color: '#ff6a00'
} as any}
><MdLocalFireDepartment className='' /></div>
<div
className='spark'
style={{
"--offsetX": 70,
"--animationSpeed": 2.5,
"--scale": 1,
"animationIterationCount": 'infinite',
"animationName": 'fly-spark-1',
color: '#ff6a00'
} as any}
><MdLocalFireDepartment className='' /></div>
{sparks.map(spark =>
<div
key={spark.id}
className='spark'
style={{
"--offsetX": spark.offsetX,
"--animationSpeed": spark.animationSpeed,
"--scale": spark.scale,
"animationName": spark.animation,
"color": spark.color
} as any}
><MdLocalFireDepartment className='' /></div>)
}
</Button>
)
}

View File

@@ -0,0 +1,79 @@
.tip-button {
--scale: 0;
transition: background-color 1s;
background-color: hsl(25, 100%, max(calc((95 - var(--scale) / 4) * 1%), 63%));
}
.tip-counter {
--pos-y: 0;
position: absolute;
left: 50%;
bottom: 110%;
color: hsl(25, 100%, 50%);
font-weight: bold;
font-size: 21px;
will-change: transform;
opacity: min(calc(var(--scale) * 1), 1);
transform: translate(-50%, max(calc(-1px * var(--scale) / 10), -30px))
scale(calc(1 + min(calc(var(--scale) / 150), 2)));
text-shadow: 0 0 4px hsl(25, 100%, 50%);
}
.spark {
position: absolute;
bottom: 46%;
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

@@ -76,6 +76,16 @@ svg {
background-color: #999;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.no-scrollbar {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Claim_CopySignatureCard from './Claim_CopySignatureCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Claim/Copy Signature Card',

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Claim_FundWithdrawCard from './Claim_FundWithdrawCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Claim/Fund Withdraw Card',

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Claim_GenerateSignatureCard from './Claim_GenerateSignatureCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Claim/Generate Signature Card',

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import Claim_SubmittedCard from './Claim_SubmittedCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Claim/Submitted Card',

View File

@@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import ProjectCard from './ProjectCard';
import ProjectCardSkeleton from './ProjectCard.Skeleton';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Project/Project Card',
@@ -14,7 +14,12 @@ export default {
const Template: ComponentStory<typeof ProjectCard> = (args) => <ProjectCard {...args} />;
export const Default = Template.bind({});
export const Default = Template.bind({
});
Default.args = {
projectId: '3'
}

View File

@@ -12,6 +12,7 @@ import { requestProvider } from 'webln';
import { PROJECT_BY_ID_QUERY, PROJECT_BY_ID_RES, PROJECT_BY_ID_VARS } from './query'
import { AiFillThunderbolt } from 'react-icons/ai';
import ProjectCardSkeleton from './ProjectCard.Skeleton'
import TipButton from 'src/Components/TipButton/TipButton';
interface Props extends ModalCard {
@@ -46,6 +47,7 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
const onConnectWallet = async () => {
try {
const webln = await requestProvider();
if (webln) {
dispatch(connectWallet(webln));
@@ -59,15 +61,16 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
}
}
const onTip = () => {
const onTip = (tip?: number) => {
if (!isWalletConnected) {
dispatch(scheduleModal({ Modal: 'TipCard' }))
dispatch(scheduleModal({ Modal: 'TipCard', props: { tipValue: tip } }))
dispatch(openModal({
Modal: 'Login_ScanningWalletCard'
}))
} else
dispatch(openModal({ Modal: 'TipCard' }))
dispatch(openModal({ Modal: 'TipCard', props: { tipValue: tip } }))
}
@@ -111,7 +114,7 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
<div className="flex-shrink-0 hidden md:flex ml-auto gap-16">
<Button color='primary' size='md' className=" my-16">Play <BsJoystick /></Button>
{isWalletConnected ?
<Button onClick={onTip} size='md' className="border border-warning-100 bg-warning-50 hover:bg-warning-50 active:bg-warning-100 my-16">Tip <MdLocalFireDepartment className='text-fire' /></Button>
<TipButton onTip={onTip} />
:
<Button onClick={onConnectWallet} size='md' className="border border-gray-200 bg-gray-100 hover:bg-gray-50 active:bg-gray-100 my-16">Connect Wallet to Vote</Button>
}
@@ -121,7 +124,7 @@ export default function ProjectCard({ onClose, direction, projectId, ...props }:
<div className="md:hidden">
<Button color='primary' size='md' fullWidth className="w-full mt-24 mb-16">Play <BsJoystick /></Button>
{isWalletConnected ?
<Button size='md' fullWidth className="bg-yellow-100 hover:bg-yellow-50 mb-24" onClick={onTip}>Vote <MdLocalFireDepartment className='text-fire' /></Button>
<TipButton fullWidth onTip={onTip} />
:
<Button size='md' fullWidth className="bg-gray-200 hover:bg-gray-100 mb-24" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet to Vote</Button>
}

View File

@@ -2,7 +2,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import TipCard from './TipCard';
import { ModalsDecorator } from '.storybook/helpers'
import { ModalsDecorator } from 'src/utils/storybookDecorators'
export default {
title: 'Tip/Tip Card',

View File

@@ -49,10 +49,10 @@ mutation Mutation($paymentRequest: String!, $preimage: String!) {
`;
interface Props extends ModalCard {
tipValue?: number;
}
export default function TipCard({ onClose, direction, ...props }: Props) {
export default function TipCard({ onClose, direction, tipValue, ...props }: Props) {
const { width, height } = useWindowSize()
const { isWalletConnected, webln } = useAppSelector(state => ({
@@ -63,7 +63,7 @@ export default function TipCard({ onClose, direction, ...props }: Props) {
const dispatch = useAppDispatch();
const [selectedOption, setSelectedOption] = useState(10);
const [voteAmount, setVoteAmount] = useState<number>(10);
const [voteAmount, setVoteAmount] = useState<number>(tipValue ?? 10);
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.DEFAULT);
const [vote, { data }] = useMutation(VOTE, {

View File

@@ -34,13 +34,16 @@ export const ALL_MODALS = {
Claim_FundWithdrawCard,
}
type ExcludeBaseModalProps<U> = Omit<U, keyof ModalCard>
type ModalProps<M extends keyof typeof ALL_MODALS> = ExcludeBaseModalProps<ComponentProps<typeof ALL_MODALS[M]>>
type NonNullableObject<T> = {
[K in keyof T]-?: NonNullable<T[K]>
}
type ModalAction<U extends keyof typeof ALL_MODALS = keyof typeof ALL_MODALS> = U extends any ?
{} extends ModalProps<U> ?
{} extends NonNullableObject<ModalProps<U>> ?
{ Modal: U }
:
{ Modal: U, props: ModalProps<U> }
@@ -48,6 +51,7 @@ type ModalAction<U extends keyof typeof ALL_MODALS = keyof typeof ALL_MODALS> =
never;
interface OpenModal {
Modal: ModalAction['Modal'],
props?: any;
@@ -110,7 +114,6 @@ export const modalSlice = createSlice({
) {
state.direction = Direction.START;
state.isOpen = true;
let props: any = {};
if ('props' in action.payload) props = { ...action.payload.props }

View File

@@ -7,9 +7,11 @@ interface StoreState {
const initialState = {
navHeight: 88,
isMobileScreen: false,
isMobileScreen: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) || window.innerWidth < 480,
} as StoreState;
export const themeSlice = createSlice({
name: "theme",
initialState,

View File

@@ -1,3 +1,7 @@
export function random(min: number, max: number) {
return Math.random() * (max - min) + min;
}
export function randomItem(...args: any[]) {
return args[Math.floor(Math.random() * args.length)];
}

View File

@@ -1,2 +1,3 @@
export * from "./storeHooks";
export * from "./useResizeListener";
export * from "./usePressHolder";

View File

@@ -0,0 +1,40 @@
import { useRef } from "react";
export const usePressHolder = (onHold: () => any, holdThreshold: number = 400) => {
const ref = useRef({
cntr: 0,
timerID: 0,
previousTimestamp: -1
});
const onPressDown = () => {
requestAnimationFrame(timer)
}
const onPressUp = () => {
cancelAnimationFrame(ref.current.timerID);
ref.current.cntr = 0;
ref.current.previousTimestamp = -1;
}
function timer(timestamp: number) {
if (ref.current.previousTimestamp === -1) ref.current.previousTimestamp = timestamp;
const dt = timestamp - ref.current.previousTimestamp;
ref.current.previousTimestamp = timestamp;
if (ref.current.cntr < holdThreshold) {
ref.current.cntr += dt;
} else {
onHold();
}
ref.current.timerID = requestAnimationFrame(timer);
}
return { onPressUp, onPressDown }
}

View File

@@ -1,7 +1,8 @@
import { DecoratorFn } from '@storybook/react';
import { AnimatePresence, motion } from 'framer-motion';
import Modal from 'src/Components/Modals/Modal/Modal';
export const ModalsDecorator = (Story: any) => {
export const ModalsDecorator: DecoratorFn = (Story) => {
const onClose = () => { };
return (
<motion.div
@@ -20,4 +21,10 @@ export const ModalsDecorator = (Story: any) => {
</AnimatePresence>
</motion.div>
);
}
export const centerDecorator: DecoratorFn = (Story) => {
return <div className="min-h-screen flex justify-center items-center">
<Story />
</div>
}