mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-09 08:34:23 +01:00
feat: Created an animated Tip button
This commit is contained in:
@@ -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])
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} />
|
||||
|
||||
17
src/Components/TipButton/TipButton.stories.tsx
Normal file
17
src/Components/TipButton/TipButton.stories.tsx
Normal 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({});
|
||||
|
||||
176
src/Components/TipButton/TipButton.tsx
Normal file
176
src/Components/TipButton/TipButton.tsx
Normal 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>
|
||||
|
||||
)
|
||||
}
|
||||
79
src/Components/TipButton/tipbutton.style.css
Normal file
79
src/Components/TipButton/tipbutton.style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./storeHooks";
|
||||
export * from "./useResizeListener";
|
||||
export * from "./usePressHolder";
|
||||
|
||||
40
src/utils/hooks/usePressHolder.ts
Normal file
40
src/utils/hooks/usePressHolder.ts
Normal 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 }
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
Reference in New Issue
Block a user