Skip to content

Instantly share code, notes, and snippets.

@cziem
Last active July 15, 2019 16:53
Show Gist options
  • Select an option

  • Save cziem/989e8fbcdbf697c735e76792b2f944e3 to your computer and use it in GitHub Desktop.

Select an option

Save cziem/989e8fbcdbf697c735e76792b2f944e3 to your computer and use it in GitHub Desktop.
PayCard Functionality
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-eqX-UA-Compatible content="ie=edge" />
<title>Mini App</title>
<style>
body {
margin: 0;
padding: 1em;
background-color: #fff;
}
[data-cart-info],
[data-credit-card] {
transform: scale(0.78);
margin-left: -3.4em;
}
[data-cc-info] input:focus,
[data-cc-digits] input:focus {
outline: none;
}
[data-cart-info] span {
display: inline-block;
vertical-align: middle;
}
.mdc-card__primary-action,
.mdc-card__primary-action:hover {
cursor: auto;
padding: 20px;
min-height: inherit;
}
[data-credit-card] {
width: 435px;
min-height: 240px;
border-radius: 10px;
background-color: #5d6874;
}
img[data-card-type] {
display: block;
width: 120px;
height: 60px;
}
[data-cc-digits] {
margin-top: 2em;
}
[data-cc-digits] input {
color: white;
font-size: 2em;
line-height: 2em;
border: none;
background: transparent;
margin-right: 0.5em;
}
[data-cc-info] {
margin-top: 1em;
}
[data-cc-info] input {
color: white;
font-size: 1.2em;
border: none;
background: transparent;
}
[data-cc-info] input:nth-child(2) {
padding-right: 10px;
float: right;
}
[data-credit-card] [data-card-type] {
transition: width 1.5s;
margin-left: calc(100% - 130px);
}
[data-pay-btn] {
position: fixed;
width: 90%;
border: 1px solid;
bottom: 20px;
}
.material-icons {
font-size: 150px;
}
[data-credit-card].is-visa {
background: linear-gradient(135deg, #622774 0%, #c53364 100%);
}
[data-credit-card].is-mastercard {
background: linear-gradient(135deg, #65799b 0%, #5e2563 100%);
}
.is-visa [data-card-type],
.is-mastercard [data-card-type] {
width: auto;
}
input.is-invalid,
.is-invalid input {
text-decoration: line-through;
}
::placeholder {
color: #fff;
}
</style>
</head>
<body>
<div data-cart-info>
<h1 class="mdc-typography--headline4">
<span class="material-icons">shopping_cart</span>
<span data-bill></span>
</h1>
</div>
<div data-credit-card class="mdc-card mdc-card--outlined">
<div class="mdc-card__primary-action">
<img src="https://placehold.it/120x60.png?text=Card" data-card-type />
<div data-cc-digits>
<input size="4" placeholder="----" type="text" />
<input size="4" placeholder="----" type="text" />
<input size="4" placeholder="----" type="text" />
<input size="4" placeholder="----" type="text" />
</div>
<div data-cc-info>
<input type="text" size="20" placeholder="Name Surname" />
<input type="text" size="6" placeholder="MM/YY" />
</div>
</div>
</div>
<button class="mdc-button" data-pay-btn>Pay & Checkout Now</button>
<script>
const appState = {}
const supportedCards = {
visa, mastercard
};
const countries = [
{
code: "US",
currency: "USD",
country: 'United States'
},
{
code: "NG",
currency: "NGN",
country: 'Nigeria'
},
{
code: 'KE',
currency: 'KES',
country: 'Kenya'
},
{
code: 'UG',
currency: 'UGX',
country: 'Uganda'
},
{
code: 'RW',
currency: 'RWF',
country: 'Rwanda'
},
{
code: 'TZ',
currency: 'TZS',
country: 'Tanzania'
},
{
code: 'ZA',
currency: 'ZAR',
country: 'South Africa'
},
{
code: 'CM',
currency: 'XAF',
country: 'Cameroon'
},
{
code: 'GH',
currency: 'GHS',
country: 'Ghana'
}
];
const formatAsMoney = (amount, buyerCountry) => {
const country = countries
.filter(data => data.country === buyerCountry)
const { code, currency } = country[0];
if (!country) {
return amount.toLocaleString("en-US", { style: 'currency', currency: "USD" })
} else {
return amount.toLocaleString(`en-${code}`, { style: 'currency', currency })
}
}
const flagIfInvalid = (field, isValid) => {
if (isValid) {
field.classList.remove('is-invalid')
} else {
field.classList.add('is-invalid')
}
}
const expiryDateFormatIsValid = (target) => {
const regex = /^\d{2,2}\/\d{2,2}/;
return regex.test(target)
}
const detectCardType = ({ target }) => {
const card = document.querySelector('[data-credit-card]')
const img = document.querySelector('[data-card-type]')
if (target.value.startsWith('5')) {
card.classList.add('is-mastercard');
card.classList.remove('is-visa');
img.src = supportedCards.mastercard;
return 'is-mastercard';
} else if (target.value.startsWith('4')) {
card.classList.add('is-visa');
card.classList.remove('is-mastercard');
img.src = supportedCards.visa;
return 'is-visa';
} else {
return 'not-supported';
}
}
const validateCardExpiryDate = ({ target }) => {
const isInTheFuture = () => {
const dateStr = target.value;
const dateArr = dateStr.split("/");
const cardDate = new Date(2000 + Number.parseInt(dateArr[1]), Number.parseInt(dateArr[0] - 1));
const today = new Date();
return cardDate.getTime() > today.getTime();
}
if (expiryDateFormatIsValid(target.value) && isInTheFuture()) {
flagIfInvalid(target, true);
return true;
} else {
flagIfInvalid(target, false)
return false;
}
}
const validateCardHolderName = ({ target }) => {
const validNames = /^[\w]{3,}/;
const names = target.value.split(' ')
const [firstName, lastName] = names;
if (validNames.test(firstName) && validNames.test(lastName) && names.length === 2) {
flagIfInvalid(target, true);
return true;
} else {
flagIfInvalid(target, false);
return false;
}
}
const validateWithLuhn = (digits) => {
return digits.reduceRight((prev, curr, idx) => {
prev = parseInt(prev, 10);
if ((idx + 1) % 2 !== 0) {
curr = (curr * 2).toString().split('').reduce((p, c) => parseInt(p, 10) + parseInt(c, 10));
}
return prev + parseInt(curr, 10)
}) % 10 === 0
}
const validateCardNumber = () => {
const cardDigits = document.querySelector('[data-cc-digits]')
let cardNumber = '';
const cardInputs = Array.from(cardDigits.querySelectorAll('input'));
cardInputs.map(({value}) => {
if (value.length === 4) {
return cardNumber += value;
}
})
const cardArray = [...cardNumber]
const validCardNum = validateWithLuhn(cardArray)
if (!validCardNum) {
cardDigits.classList.add('is-invalid')
return false
} else if (validCardNum) {
cardDigits.classList.remove('is-invalid')
return true
}
}
const uiCanInteract = () => {
const ccDigits = document.querySelector('[data-cc-digits]')
const inputs = ccDigits.querySelectorAll('input')
inputs[0].addEventListener('blur', detectCardType)
const info = document.querySelector('[data-cc-info]')
const infoDetails = info.querySelectorAll('input')
infoDetails[0].addEventListener('blur', validateCardHolderName)
infoDetails[1].addEventListener('blur', validateCardExpiryDate)
document.querySelector('[data-pay-btn]')
.addEventListener('click', validateCardNumber)
inputs[0].focus()
}
const displayCartTotal = ({ results }) => {
const [ data ] = results;
const { itemsInCart, buyerCountry } = data;
appState.items = itemsInCart;
appState.country = buyerCountry;
appState.bill = itemsInCart.reduce((acc, cur) => acc + (cur.price * cur.qty), 0);
appState.billFormatted = formatAsMoney(appState.bill, appState.country);
document.querySelector('[data-bill]').textContent = appState.billFormatted;
uiCanInteract();
}
const fetchBill = () => {
const api = "https://randomapi.com/api/006b08a801d82d0c9824dcfdfdfa3b3c"
fetch(api)
.then(response => response.json())
.then(data => displayCartTotal(data))
.catch(err => console.log("Error: ", err));
}
const startApp = () => {
fetchBill();
};
startApp();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment