Create SavedCardDetailsComponent

This commit is contained in:
Jenni Nurmi 2019-08-08 10:47:38 +03:00
parent 89e7846519
commit 40a3ab09aa
6 changed files with 542 additions and 0 deletions

View file

@ -0,0 +1,236 @@
@import '../../marketplace.css';
.root {
}
.savedPaymentMethodTitle {
@apply --marketplaceH4FontStyles;
font-weight: bold;
cursor: pointer;
}
.savedPaymentMethod {
display: inline-block;
margin-right: auto;
}
.paymentMethodPlaceholderDesktop {
display: none;
}
.paymentMethodPlaceholderMobile {
display: inline-block;
}
@media (--viewportSmall) {
.paymentMethodPlaceholderDesktop {
display: inline-block;
}
.paymentMethodPlaceholderMobile {
display: none;
}
}
.cardIcon {
margin: 0 12px 0 4px;
width: 24px;
height: 16px;
display: inline-block;
}
.closeIcon {
@apply --marketplaceModalCloseIcon;
margin-left: 0;
margin-right: 5px;
}
.iconCheckmark {
stroke: var(--successColor);
display: inline-block;
margin: 12px 0 0 24px;
}
.iconCheckmarkHidden {
visibility: hidden;
display: inline-block;
margin: 0 0 0 24px;
}
.expirationDate {
padding-left: 24px;
}
.expirationDateExpired {
padding-left: 24px;
color: var(--failColor);
}
.cardExpiredText {
@apply --marketplaceH4FontStyles;
color: var(--failColor);
font-weight: var(--fontWeightSemiBold);
margin-top: 13px;
margin-bottom: 11px;
}
.menu {
max-width: 100%;
height: auto;
}
.menuLabel {
text-align: left;
font-weight: 500;
padding: 0;
border-radius: 4px;
border: 1px solid var(--matterColorNegative);
transition: all 0.2s ease-out;
z-index: calc(var(--zIndexPopup) + 1);
position: relative;
&:hover {
background-color: var(--matterColorLight);
border: 1px solid var(--matterColorAnti);
}
&:focus {
outline: none;
background-color: var(--matterColorLight);
border: 1px solid var(--matterColorNegative);
text-decoration: none;
border-radius: 4px 4px 0px 0px;
}
}
.menuLabelWrapper,
.menuLabelWrapperExpired {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
padding: 12px 24px 18px 16px;
}
.menuLabelWrapperExpired {
border: 1px solid var(--failColor);
}
.menuContent {
position: absolute;
z-index: var(--zIndexPopup);
background-color: var(--matterColorLight);
box-shadow: var(--boxShadowFilterButton);
border-radius: 4px;
border: 1px solid var(--matterColorNegative);
transition: all 0.15s ease-out;
/* When the dropdown is open, we need to hack the box-shadow's position
to fit the whole dropdown menu component with padding-top
MenuLabel height 56px + original padding-top 7px results to 63px
*/
padding: 63px 0 15px 0;
top: 0px;
}
.menuItem {
color: var(--matterColor);
&:hover {
background-color: #f2f2f2;
}
}
.menuText,
.menuTextReplaceCard {
@apply --marketplaceMessageFontStyles;
padding: 12px 8px;
color: var(--matterColor);
text-align: left;
font-weight: 500;
width: 80%;
&:hover {
text-decoration: none;
}
&:focus {
outline: 0;
background-color: #f2f2f2;
}
}
.menuTextReplaceCard svg {
position: relative;
top: -2px;
width: 22px;
margin: 0 10px 0 4px;
}
.menuDivider {
@apply --marketplaceH5FontStyles;
color: var(--matterColorAnti);
padding: 14px 24px 2px 24px;
margin: 0;
}
.iconArrow {
stroke: var(--matterColor);
fill: var(--matterColor);
transition: all 0.2s ease-out;
}
.IconArrowAnimation {
transform: rotate(180deg);
}
.savedPaymentMethodDelete {
@apply --marketplaceH4FontStyles;
color: var(--matterColorAnti);
cursor: pointer;
margin-top: 14px;
margin-bottom: 10px;
&:hover {
color: var(--matterColor);
}
&:focus {
outline: none;
color: var(--matterColor);
}
}
.modalButtonsWrapper {
padding-top: 80px;
display: flex;
justify-content: flex-end;
align-items: baseline;
}
/* Title of the modal */
.modalTitle {
@apply --marketplaceModalTitleStyles;
padding-top: 36px;
}
/* Paragraph for the Modal */
.modalMessage {
@apply --marketplaceModalParagraphStyles;
min-height: 200px;
}
.cancelCardDelete {
color: var(--matterColorAnti);
cursor: pointer;
width: 50%;
text-align: right;
padding-right: 64px;
&:hover {
color: var(--matterColor);
}
}

View file

@ -0,0 +1,53 @@
import { fakeIntl } from '../../util/test-data';
import SavedCardDetails from './SavedCardDetails';
const noop = () => null;
const defaultProps = {
intl: fakeIntl,
onDeleteCard: noop,
onChange: noop,
onManageDisableScrolling: noop,
};
export const SavedCardDetailsExample = {
component: SavedCardDetails,
props: {
...defaultProps,
card: {
brand: 'visa',
expirationMonth: 10,
expirationYear: 2050,
last4Digits: '3220',
},
},
group: 'payment methods',
};
export const SavedCardDetailsNoDelete = {
component: SavedCardDetails,
props: {
...defaultProps,
card: {
brand: 'mastercard',
expirationMonth: 10,
expirationYear: 2050,
last4Digits: '3220',
},
onDeleteCard: null,
},
group: 'payment methods',
};
export const SavedCardDetailsExpired = {
component: SavedCardDetails,
props: {
...defaultProps,
card: {
brand: 'amex',
expirationMonth: 7,
expirationYear: 2019,
last4Digits: '3220',
},
},
group: 'payment methods',
};

View file

@ -0,0 +1,240 @@
import React, { useState } from 'react';
import { bool, func, number, shape, string } from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from 'react-intl';
import {
IconArrowHead,
IconCard,
IconClose,
IconCheckmark,
Button,
InlineTextButton,
Menu,
MenuLabel,
MenuItem,
MenuContent,
Modal,
} from '../../components';
import css from './SavedCardDetails.css';
const DEFAULT_CARD = 'defaultCard';
const REPLACE_CARD = 'replaceCard';
const SavedCardDetails = props => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [active, setActive] = useState(DEFAULT_CARD);
const {
rootClassName,
className,
intl,
card,
onChange,
onDeleteCard,
onManageDisableScrolling,
deletePaymentMethodInProgress,
} = props;
const { last4Digits, expirationMonth, expirationYear, brand } = card || {};
const classes = classNames(rootClassName || css.root, className);
const paymentMethodPlaceholderDesktop = intl.formatMessage(
{ id: 'SavedCardDetails.savedPaymentMethodPlaceholderDesktop' },
{ lastFour: last4Digits }
);
const paymentMethodPlaceholderMobile = intl.formatMessage(
{ id: 'SavedCardDetails.savedPaymentMethodPlaceholderMobile' },
{ lastFour: last4Digits }
);
const paymentMethodPlaceholder = (
<>
<span className={css.paymentMethodPlaceholderDesktop}>{paymentMethodPlaceholderDesktop}</span>
<span className={css.paymentMethodPlaceholderMobile}>{paymentMethodPlaceholderMobile}</span>
</>
);
const replaceCardText = intl.formatMessage({
id: 'SavedCardDetails.replaceCardText',
});
const replaceCard = (
<span>
<IconCard brand="none" className={css.cardIcon} /> {replaceCardText}
</span>
);
const expiredCardText = intl.formatMessage(
{ id: 'SavedCardDetails.expiredCardText' },
{ lastFour: last4Digits }
);
const expiredText = <div className={css.cardExpiredText}>{expiredCardText}</div>;
const isExpired = (expirationMonth, expirationYear) => {
const currentTime = new Date();
const currentYear = currentTime.getFullYear();
const currentMonth = currentTime.getMonth() + 1; //getMonth() method returns the month (from 0 to 11)
if (expirationYear < currentYear) {
return true;
} else if (expirationYear === currentYear && expirationMonth < currentMonth) {
return true;
}
return false;
};
const isCardExpired =
expirationMonth && expirationYear && isExpired(expirationMonth, expirationYear);
const defaultCard = (
<div className={css.savedPaymentMethod}>
<IconCard brand={brand} className={css.cardIcon} />
{paymentMethodPlaceholder}
<span className={isCardExpired ? css.expirationDateExpired : css.expirationDate}>
{expirationMonth}/{expirationYear.toString().substring(2)}
</span>
</div>
);
const handleClick = item => e => {
// Clicking buttons inside a form will call submit
e.preventDefault();
e.stopPropagation();
setActive(item);
setMenuOpen(false);
if (onChange) {
onChange(item);
}
};
const onToggleActive = isOpen => {
setMenuOpen(isOpen);
};
const handleDeleteCard = () => {
setIsModalOpen(true);
};
const iconArrowClassName = menuOpen ? css.IconArrowAnimation : null;
const replaceCardTitle = intl.formatMessage({
id: 'SavedCardDetails.replaceCardTitle',
});
const removeCardModalTitle = intl.formatMessage({ id: 'SavedCardDetails.removeCardModalTitle' });
const removeCardModalContent = intl.formatMessage({
id: 'SavedCardDetails.removeCardModalContent',
});
const cancel = intl.formatMessage({ id: 'SavedCardDetails.cancel' });
const removeCard = intl.formatMessage({ id: 'SavedCardDetails.removeCard' });
const deletePaymentMethod = intl.formatMessage({ id: 'SavedCardDetails.deletePaymentMethod' });
const showExpired = isCardExpired && active === DEFAULT_CARD;
return (
<div className={classes}>
<Menu className={css.menu} isOpen={menuOpen} onToggleActive={onToggleActive} useArrow={false}>
<MenuLabel className={css.menuLabel}>
<div className={showExpired ? css.menuLabelWrapperExpired : css.menuLabelWrapper}>
{active === DEFAULT_CARD ? defaultCard : replaceCard}
<span>
<IconArrowHead
direction="down"
size="small"
rootClassName={css.iconArrow}
className={iconArrowClassName}
/>
</span>
</div>
</MenuLabel>
<MenuContent rootClassName={css.menuContent}>
<MenuItem key="first item" className={css.menuItem}>
<IconCheckmark
className={active === DEFAULT_CARD ? css.iconCheckmark : css.iconCheckmarkHidden}
size="small"
/>
<InlineTextButton className={css.menuText} onClick={handleClick(DEFAULT_CARD)}>
{defaultCard}
</InlineTextButton>
</MenuItem>
<MenuItem key="divider" className={css.menuDivider}>
{replaceCardTitle}
</MenuItem>
<MenuItem key="second item" className={css.menuItem}>
<IconCheckmark
className={active === REPLACE_CARD ? css.iconCheckmark : css.iconCheckmarkHidden}
size="small"
/>
<InlineTextButton
className={css.menuTextReplaceCard}
onClick={handleClick(REPLACE_CARD)}
>
{replaceCard}
</InlineTextButton>
</MenuItem>
</MenuContent>
</Menu>
{showExpired && !menuOpen ? expiredText : null}
{onDeleteCard && active !== REPLACE_CARD ? (
<InlineTextButton onClick={handleDeleteCard} className={css.savedPaymentMethodDelete}>
<IconClose rootClassName={css.closeIcon} size="small" />
{deletePaymentMethod}
</InlineTextButton>
) : null}
<Modal
id="VerifyDeletingPaymentMethod"
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
}}
contentClassName={css.modalContent}
onManageDisableScrolling={onManageDisableScrolling}
>
<div>
<div className={css.modalTitle}>{removeCardModalTitle}</div>
<p className={css.modalMessage}>{removeCardModalContent}</p>
<div className={css.modalButtonsWrapper}>
<div onClick={() => setIsModalOpen(false)} className={css.cancelCardDelete}>
{cancel}
</div>
<Button onClick={onDeleteCard} inProgress={deletePaymentMethodInProgress}>
{removeCard}
</Button>
</div>
</div>
</Modal>
</div>
);
};
SavedCardDetails.defaultProps = {
rootClassName: null,
className: null,
card: null,
onChange: null,
onDeleteCard: null,
deletePaymentMethodInProgress: false,
};
SavedCardDetails.propTypes = {
rootClassName: string,
className: string,
intl: intlShape.isRequired,
card: shape({
brand: string.isRequired,
expirationMonth: number.isRequired,
expirationYear: number.isRequired,
last4Digits: string.isRequired,
}),
onChange: func,
onDeleteCard: func,
onManageDisableScrolling: func.isRequired,
deletePaymentMethodInProgress: bool,
};
export default injectIntl(SavedCardDetails);

View file

@ -128,6 +128,7 @@ export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDis
export { default as Page } from './Page/Page';
export { default as PriceFilter } from './PriceFilter/PriceFilter';
export { default as Reviews } from './Reviews/Reviews';
export { default as SavedCardDetails } from './SavedCardDetails/SavedCardDetails';
export { default as SearchFilters } from './SearchFilters/SearchFilters';
export { default as SearchFiltersMobile } from './SearchFiltersMobile/SearchFiltersMobile';
export { default as SearchFiltersPanel } from './SearchFiltersPanel/SearchFiltersPanel';

View file

@ -59,6 +59,7 @@ import * as RangeSlider from './components/RangeSlider/RangeSlider.example';
import * as ResponsiveImage from './components/ResponsiveImage/ResponsiveImage.example';
import * as ReviewRating from './components/ReviewRating/ReviewRating.example';
import * as Reviews from './components/Reviews/Reviews.example';
import * as SavedCardDetails from './components/SavedCardDetails/SavedCardDetails.example';
import * as SectionThumbnailLinks from './components/SectionThumbnailLinks/SectionThumbnailLinks.example';
import * as SelectMultipleFilter from './components/SelectMultipleFilter/SelectMultipleFilter.example';
import * as StripeBankAccountTokenInputField from './components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example';
@ -171,6 +172,7 @@ export {
ReviewForm,
ReviewRating,
Reviews,
SavedCardDetails,
SectionThumbnailLinks,
SelectMultipleFilter,
SendMessageForm,

View file

@ -684,6 +684,16 @@
"ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.",
"ReviewModal.later": "Later",
"ReviewModal.title": "Leave a review for {revieweeName}",
"SavedCardDetails.cancel": "Cancel",
"SavedCardDetails.deletePaymentMethod": "Delete payment card",
"SavedCardDetails.expiredCardText": "This payment card has expired",
"SavedCardDetails.removeCard": "Remove card",
"SavedCardDetails.removeCardModalContent": "The card will be removed from your account and can't be used for future purchases.",
"SavedCardDetails.removeCardModalTitle": "Are you sure you want to remove this credit card?",
"SavedCardDetails.replaceCardText": "Credit or debit card",
"SavedCardDetails.replaceCardTitle": "Use another payment method",
"SavedCardDetails.savedPaymentMethodPlaceholderDesktop": "•••• •••• •••• {lastFour}",
"SavedCardDetails.savedPaymentMethodPlaceholderMobile": "…{lastFour}",
"SearchFilters.amenitiesLabel": "Amenities",
"SearchFilters.categoryLabel": "Category",
"SearchFilters.filtersButtonLabel": "Filters",