Skip to content

Commit

Permalink
Merge pull request #66 from fs-jun24-team-3/hotfix/prevent-to-add-pro…
Browse files Browse the repository at this point in the history
…duct-with-phone-card-button-again

hotfix/prevent-to-add-product-with-phone-card-button-again
  • Loading branch information
k-marchuk authored Sep 25, 2024
2 parents 1109685 + 03875e6 commit 111c265
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 72 deletions.
9 changes: 7 additions & 2 deletions src/components/Buttons/WideButton.module.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
.wide_button {
background-color: $wide-button-bg-color-default;
font-size: 14px;
font-weight: 700;
color: $wide-button-text-color-default;
border-radius: 48px;
border: none;
transition: box-shadow, background-color, 0.1s;
width: 100%;
@include phoneCardWideButtonHeight;

&__title {
font-size: 14px;
font-weight: 700;
}

&:hover {
box-shadow: $wide-button-box-shadow-on-hover;
Expand Down
236 changes: 174 additions & 62 deletions src/components/Buttons/WideButton.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,229 @@
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import styles from './WideButton.module.scss';
import anime from 'animejs';
import classNames from 'classnames';

type Props = {
onClick?: () => void;
onClickForCancel?: () => void;
buttonTitle: string;
styleList?: {
[key: string]: number | string | [];
};
animationSettings?: anime.AnimeParams;
useSucceessAnimation?: boolean;
useAnimationForPhoneCard?: boolean;
isError?: boolean;
animationTimeMS?: number;
};

export const WideButton: React.FC<Props> = ({
buttonTitle,
styleList,
onClick = () => {},
onClickForCancel = () => {},
animationSettings = {},
useSucceessAnimation = true,
useSucceessAnimation = false,
useAnimationForPhoneCard = false,
isError = false,
animationTimeMS = 0,
}) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const [isAnimatingError] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [animationTimeSum] = useState(
useSucceessAnimation ? 2000 + animationTimeMS : animationTimeMS,
);
const isAnimatingRef = useRef(false);
const [isAnimatingError, setIsAnimatingError] = useState(false);
const animationInstance = useRef<anime.AnimeInstance | null>(null);
const [clicksCount, setClicksCount] = useState(1);
const [key, setKey] = useState<string | null>(null);

const handleDefaultButtonClickAction = () => {
setTimeout(() => {
anime({
targets: buttonRef.current,
backgroundColor: '#fff',
color: '#31FC56',
duration: 1000,
keyframes: [
{ rotateX: -180, translateY: -28 },
{ rotateX: -360, translateY: 0 },
],
textShadow: ['rgba(0,255, 0, 0.7) 0px 0px 29px'],
boxShadow: [
'rgba(0,0,0,0.9) 0px 0px 0px;',
{ value: 'rgba(0,255, 0, 0.1) 0px 0px 19px', duration: 700 },
{ value: 'rgba(0,255, 0, 0.9) 0px 0px 29px', duration: 300 },
'rgba(0,255, 0, 0.2) 0px 0px 29px',
],
easing: 'easeInOutQuad',
begin: () => {
setTimeout(() => {
buttonRef.current!.textContent = '';
}, 600);
},
complete: () => {
animationInstance.current = anime({
targets: buttonRef.current,
backgroundColor: '#fff',
color: '#31FC56',
fontSize: 14,
fontWeight: 600,
duration: 1000,
keyframes: [
{ rotateX: -180, translateY: -28 },
{ rotateX: -360, translateY: 0 },
],
textShadow: ['rgba(0,255, 0, 0.7) 0px 0px 29px'],
boxShadow: [
'rgba(0,0,0,0.9) 0px 0px 0px;',
{ value: 'rgba(0,255, 0, 0.1) 0px 0px 19px', duration: 700 },
{ value: 'rgba(0,255, 0, 0.9) 0px 0px 29px', duration: 300 },
'rgba(0,255, 0, 0.2) 0px 0px 29px',
],
easing: 'easeInOutQuad',
begin: () => {
if (buttonRef.current) {
buttonRef.current.textContent = '';
}
},
complete: () => {
if (buttonRef.current) {
buttonRef.current!.textContent = 'Success';
setTimeout(() => {
onClick();
setIsAnimating(false);
}, 1200);
},
});
}, 400);
}
},
});
};

const handleAnimationForPhoneCard = () => {
setClicksCount(prev => prev + 1);
switch (clicksCount) {
case 1: {
handleDefaultButtonClickAction();
setTimeout(() => {
animationInstance.current = anime({
targets: buttonRef.current,
backgroundColor: '#fff',
color: '#fc0202',
fontSize: 14,
fontWeight: 700,
duration: 500,
keyframes: [{ opacity: 0 }, { opacity: 1 }],
textShadow: ['rgba(255,0, 0, 0.7) 0px 0px 29px'],
boxShadow: [
'rgba(0,0,0,0.9) 0px 0px 0px;',
{ value: 'rgba(255,0, 0, 0.1) 0px 0px 19px', duration: 300 },
{ value: 'rgba(255,0, 0, 0.9) 0px 0px 29px', duration: 200 },
'rgba(255,0, 0, 0.2) 0px 0px 29px',
],
easing: 'easeInOutQuad',
begin: () => {
onClick();
if (buttonRef.current) {
buttonRef.current.textContent = '';
}
},
complete: () => {
isAnimatingRef.current = false;
if (buttonRef.current) {
buttonRef.current!.textContent = 'Cancel';
}
},
});
}, animationTimeSum);

return;
}
case 2: {
animationInstance.current = anime({
targets: buttonRef.current,
backgroundColor: '#4219d0',
color: '#fff',
duration: 600,
fontSize: 14,
fontWeight: 700,
keyframes: [
{ transformScale: 3, filterBlur: 6 },
{ transformScale: 1, filterBlur: 0 },
],
textShadow: ['rgba(0,0, 0, 0) 0px 0px 0px'],
boxShadow: [
'rgba(0,0,0,0.9) 0px 0px 0px;',
{ value: 'rgba(0, 0, 255, 0.1) 0px 0px 19px', duration: 300 },
{ value: 'rgba(0, 0, 255, 0.9) 0px 0px 29px', duration: 200 },
{ value: 'rgba(0, 0, 255, 0.1) 0px 0px 29px', duration: 100 },
'rgba(0,0, 0, 0) 0px 0px 0px',
],
easing: 'easeInOutQuad',
begin: () => {
onClickForCancel();
if (buttonRef.current) {
buttonRef.current.textContent = '';
}
},
complete: () => {
isAnimatingRef.current = false;
if (buttonRef.current) {
buttonRef.current!.textContent = 'Add to cart';
}
},
});

return;
}
}
};

const handleError = () => {
// just for test
// setIsAnimatingError(false);
// getAccessories()
// .then(() => {
// throw 123;
// })
// .catch(() => {
// setIsAnimatingError(true);
// })
// .finally(() => {
// setTimeout(() => {
// setIsAnimatingError(false);
// }, 13000);
// });
setIsAnimatingError(true);
setTimeout(() => {
setIsAnimatingError(false);
}, 800);
};

const handleClick = () => {
if (isAnimatingError) {
handleError();
if (isError) {
if (!isAnimatingError) {
handleError();
}

return;
}

if (isAnimating) {
if (isAnimatingRef.current) {
return;
}

if (useSucceessAnimation) {
setIsAnimating(true);
handleDefaultButtonClickAction();
setIsAnimating(false);
if (useAnimationForPhoneCard && !isAnimatingRef.current) {
isAnimatingRef.current = true;
handleAnimationForPhoneCard();

if (clicksCount >= 2) {
setClicksCount(1);
}

return;
}

if (animationSettings.targets !== undefined) {
setIsAnimating(true);
anime(animationSettings);
onClick();
if (useSucceessAnimation) {
isAnimatingRef.current = true;
handleDefaultButtonClickAction();

if (isAnimatingRef.current === true) {
isAnimatingRef.current = true;
anime(animationSettings);
setTimeout(() => {
isAnimatingRef.current = false;
onClick();
}, animationTimeSum);
}

return;
} else {
if (isAnimatingRef.current) {
isAnimatingRef.current = true;
anime(animationSettings);
setTimeout(() => {
isAnimatingRef.current = false;
onClick();
}, animationTimeSum);
}

return;
}
};

useEffect(() => {
setKey(String(Math.random));
}, []);

return (
<button
key={key}
ref={buttonRef}
className={classNames(styles.wide_button, {
[styles.swing_bottom_fwd]: isAnimatingError,
[styles.shake_horizontal]: isAnimatingError,
})}
onClick={handleClick}
style={{ ...styleList }}
>
{buttonTitle}
<p className={styles['wide_button__title']}>{buttonTitle}</p>
</button>
);
};
24 changes: 18 additions & 6 deletions src/components/ProductActions/ProductActions.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
import { MainButton } from '../Buttons/MainButton';
import styles from './ProductActions.module.scss';
import React from 'react';
import { FavoriteButton } from '../Buttons/FavoriteButton';
import { ButtonSize } from '../../utils/types/ButtonSize';
import { useAppDispatch } from '../../app/reduxHooks';
import { addToCart } from '../../app/slices/cartSlise';
import { useAppDispatch, useAppSelector } from '../../app/reduxHooks';
import { addToCart, removeFromCart } from '../../app/slices/cartSlise';
import { UnionProduct } from '../../utils/types/UnionProduct';
import { addToFavorites } from '../../app/slices/favoritesSlice';
import { MainButton } from '../Buttons/MainButton';

type Props = {
size?: Exclude<ButtonSize, ButtonSize.Small>;
item?: UnionProduct;
item: UnionProduct;
};

export const ProductActions: React.FC<Props> = ({
size = ButtonSize.Default,
item,
}) => {
const dispatch = useAppDispatch();
const inCart = useAppSelector(state =>
state.cart.cartItems.some(el => el.item.id === item.id),
);

const handleAddToCart = () => {
if (item) {
dispatch(addToCart(item));
}
};
const handleRemoveFromCart = () => {
if (item) {
dispatch(removeFromCart(item.id));
}
};
const handleAddToFavorites = () => {
if (item) {
dispatch(addToFavorites(item));
}
};
return (
<div className={styles['product-actions-block']}>
<MainButton label="Add to cart" size={size} onClick={handleAddToCart} />
<MainButton
label={inCart ? 'Remove from cart' : 'Add to cart'}
size={size}
onClick={inCart ? handleRemoveFromCart : handleAddToCart}
/>
<FavoriteButton
size={size}
onClick={handleAddToFavorites}
productId={item!.id}
productId={item.id}
/>
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions src/pages/CartPage/CartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const CartPage: React.FC<Props> = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const cartItems = useAppSelector((state: RootState) => state.cart.cartItems);
const isCartEmpty = cartItems.length < 1;
const totalCost = useAppSelector((state: RootState) =>
selectTotalCost(state.cart),
);
Expand Down Expand Up @@ -51,12 +52,13 @@ export const CartPage: React.FC<Props> = () => {
buttonTitle={'Checkout'}
styleList={{
height: 48,
width: '100%',
}}
onClick={() => {
dispatch(clearCart());
navigate('/home');
navigate('/userPage');
}}
useSucceessAnimation={true}
isError={isCartEmpty}
/>
</div>
</div>
Expand Down
Loading

0 comments on commit 111c265

Please sign in to comment.