mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Create SavedCardDetailsComponent
This commit is contained in:
parent
89e7846519
commit
40a3ab09aa
6 changed files with 542 additions and 0 deletions
236
src/components/SavedCardDetails/SavedCardDetails.css
Normal file
236
src/components/SavedCardDetails/SavedCardDetails.css
Normal 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);
|
||||
}
|
||||
}
|
||||
53
src/components/SavedCardDetails/SavedCardDetails.example.js
Normal file
53
src/components/SavedCardDetails/SavedCardDetails.example.js
Normal 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',
|
||||
};
|
||||
240
src/components/SavedCardDetails/SavedCardDetails.js
Normal file
240
src/components/SavedCardDetails/SavedCardDetails.js
Normal 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);
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue