mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-26 02:34:28 +01:00
update: show loading & success state on vote-btn, fix multiline bug on story-form title input
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { centerDecorator } from 'src/utils/storybook/decorators';
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
import VoteButton from './VoteButton';
|
||||
|
||||
@@ -14,65 +15,71 @@ export default {
|
||||
|
||||
const Template: ComponentStory<typeof VoteButton> = (args) => <VoteButton {...args} />;
|
||||
|
||||
const onVoteHandler: ComponentProps<typeof VoteButton>['onVote'] = (a, c) => {
|
||||
setTimeout(() => {
|
||||
c.onSuccess?.();
|
||||
c.onSetteled?.();
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
votes: 540,
|
||||
onVote: () => { }
|
||||
onVote: onVoteHandler
|
||||
}
|
||||
|
||||
export const Vertical = Template.bind({});
|
||||
Vertical.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
direction: 'vertical'
|
||||
}
|
||||
|
||||
export const Dense = Template.bind({});
|
||||
Dense.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
dense: true
|
||||
}
|
||||
|
||||
export const FillTypeUpdown = Template.bind({});
|
||||
FillTypeUpdown.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
fillType: 'upDown'
|
||||
}
|
||||
|
||||
export const FillTypeBackground = Template.bind({});
|
||||
FillTypeBackground.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
fillType: 'background'
|
||||
}
|
||||
|
||||
export const FillTypeRadial = Template.bind({});
|
||||
FillTypeRadial.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
fillType: 'radial'
|
||||
}
|
||||
|
||||
export const NoCounter = Template.bind({});
|
||||
NoCounter.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
disableCounter: true,
|
||||
}
|
||||
|
||||
export const CounterReset = Template.bind({});
|
||||
CounterReset.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
resetCounterOnRelease: true
|
||||
}
|
||||
|
||||
export const NoShake = Template.bind({});
|
||||
NoShake.args = {
|
||||
votes: 540,
|
||||
onVote: () => { },
|
||||
onVote: onVoteHandler,
|
||||
disableShake: true,
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { random, randomItem, numberFormatter } from 'src/utils/helperFunctions'
|
||||
import { useDebouncedCallback, useMountEffect, useThrottledCallback } from '@react-hookz/web'
|
||||
import { UnionToObjectKeys } from 'src/utils/types/utils'
|
||||
import { Portal } from '../Portal/Portal'
|
||||
import { ThreeDots } from 'react-loader-spinner'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +24,11 @@ interface Particle {
|
||||
|
||||
type Props = {
|
||||
votes: number,
|
||||
onVote?: (amount: number, config: Partial<{ onSetteled: () => void }>) => void,
|
||||
onVote?: (amount: number, config: Partial<{
|
||||
onSetteled: () => void;
|
||||
onError: () => void;
|
||||
onSuccess: () => void;
|
||||
}>) => void,
|
||||
fillType?: 'leftRight' | 'upDown' | "background" | 'radial',
|
||||
direction?: 'horizontal' | 'vertical'
|
||||
disableCounter?: boolean
|
||||
@@ -44,6 +50,8 @@ const btnPadding: UnionToObjectKeys<Props, 'direction', any> = {
|
||||
} as UnionToObjectKeys<Props, 'size'>
|
||||
}
|
||||
|
||||
type BtnState = 'ready' | 'voting' | 'loading' | "success" | "fail";
|
||||
|
||||
export default function VoteButton({
|
||||
votes,
|
||||
onVote = () => { },
|
||||
@@ -64,17 +72,26 @@ export default function VoteButton({
|
||||
const totalIncrementsCountRef = useRef(0)
|
||||
const currentIncrementsCountRef = useRef(0);
|
||||
const [increments, setIncrements] = useState<Array<{ id: string, value: number }>>([]);
|
||||
const [btnPosition, setBtnPosition] = useState<{ top: number, left: number, width: number, height: number }>()
|
||||
const [btnPosition, setBtnPosition] = useState<{ top: number, left: number, width: number, height: number }>();
|
||||
const [btnState, setBtnState] = useState<BtnState>('ready');
|
||||
|
||||
const isMobileScreen = useAppSelector(s => s.ui.isMobileScreen);
|
||||
const resetCounterOnRelease = resetCounterOnReleaseProp;
|
||||
|
||||
const doVote = useDebouncedCallback(() => {
|
||||
setBtnState('loading');
|
||||
const amount = voteCntRef.current;
|
||||
onVote(amount, { onSetteled: () => setVoteCnt(v => v - amount) });
|
||||
onVote(amount, {
|
||||
onSuccess: () => setBtnState("success"),
|
||||
onError: () => setBtnState('fail'),
|
||||
onSetteled: () => {
|
||||
setVoteCnt(v => v - amount);
|
||||
setTimeout(() => setBtnState("ready"), 2000);
|
||||
}
|
||||
});
|
||||
voteCntRef.current = 0;
|
||||
|
||||
}, [], 2000)
|
||||
}, [], 1500)
|
||||
|
||||
const clickIncrement = () => {
|
||||
if (!disableShake)
|
||||
@@ -132,13 +149,14 @@ export default function VoteButton({
|
||||
const { onPressDown, onPressUp, isHolding } = usePressHolder(onHold, 100);
|
||||
|
||||
const handlePressDown = () => {
|
||||
setWasActive(true);
|
||||
if (btnState !== 'ready' && btnState !== 'voting') return;
|
||||
|
||||
setBtnState('voting');
|
||||
onPressDown();
|
||||
}
|
||||
|
||||
const handlePressUp = (event?: any) => {
|
||||
if (!wasActive) return;
|
||||
setWasActive(false);
|
||||
if (btnState !== 'voting') return;
|
||||
|
||||
if (event?.preventDefault) event.preventDefault();
|
||||
|
||||
@@ -230,6 +248,26 @@ export default function VoteButton({
|
||||
|
||||
/><span className="align-middle w-[4ch]"> {numberFormatter(votes + voteCnt)}</span>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{btnState === 'loading' &&
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={styles.loading}>
|
||||
<ThreeDots width={20} color="#dc2626" />
|
||||
</motion.div>
|
||||
}
|
||||
{btnState === 'success' &&
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={styles.success}>
|
||||
Thanks!!
|
||||
</motion.div>
|
||||
}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<Portal id='effects-container'>
|
||||
|
||||
@@ -106,6 +106,38 @@
|
||||
);
|
||||
}
|
||||
|
||||
.loading {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f9f6f6;
|
||||
font-size: 14px;
|
||||
color: #dc2626;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.success {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f9f6f6;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #dc2626;
|
||||
outline: 1px solid #ef4444;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
@keyframes fly_value {
|
||||
0% {
|
||||
transform: translate(-50%, 0) scale(0.5);
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function StoryForm(props: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode)
|
||||
titleInputRef.current?.setAttribute("style", "height:" + (titleInputRef.current.scrollHeight) + "px;overflow-y:hidden;");
|
||||
setTimeout(() => titleInputRef.current?.setAttribute("style", "height:" + (titleInputRef.current.scrollHeight) + "px;overflow-y:hidden;"), 0)
|
||||
}, [editMode])
|
||||
|
||||
|
||||
@@ -133,8 +133,9 @@ export default function StoryForm(props: Props) {
|
||||
|
||||
<div className="mt-16 relative">
|
||||
<textarea
|
||||
rows={1}
|
||||
autoFocus
|
||||
className="p-0 text-[42px] leading-[58px] border-0 max-w-full
|
||||
className="p-0 text-[42px] leading-[58px] border-0 w-full max-w-full resize-none
|
||||
focus:border-0 focus:outline-none focus:ring-0 font-bolder placeholder:!text-gray-400"
|
||||
placeholder='New story title here...'
|
||||
{...titleRegisteration}
|
||||
|
||||
Reference in New Issue
Block a user