Skip to content

Instantly share code, notes, and snippets.

@osvaldoM
Last active July 29, 2019 21:10
Show Gist options
  • Save osvaldoM/2172eccc1807a648afa00cf1431ff622 to your computer and use it in GitHub Desktop.
Save osvaldoM/2172eccc1807a648afa00cf1431ff622 to your computer and use it in GitHub Desktop.
Snippet for andela challenge
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Mini App</title>
<style>
body {
margin: 0;
padding: 1em;
background-color: white;
}
[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;
}
.mdc-card__primary-action,
.mdc-card__primary-action:hover {
cursor: auto;
padding: 20px;
min-height: inherit;
}
[data-credit-card] [data-card-type] {
transition: width 1.5s;
margin-left: calc(100% - 130px);
}
[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;
}
[data-cart-info] span {
display: inline-block;
vertical-align: middle;
}
.material-icons {
font-size: 150px
}
[data-credit-card] {
width: 435px;
min-height: 240px;
background-color: #5d6874;
border-radius: 10px
}
[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] .second-input {
padding-right: 10px;
float: right;
}
[data-pay-btn] {
position: fixed;
width: 90%;
border-style:solid;
border-width: 1px;
bottom: 20px;
}
</style>
</head>
<body>
<div data-cart-info>
<h4 class="mdc-typography--headline4">
<span class="material-icons"> shopping_cart</span>
<span data-bill></span>
</h4>
</div>
<div data-credit-card class="mdc-card mdc-card--outlined">
<div class="mdc-card__primary-action">
<img data-card-type src="https://placehold.it/120x60.png?text=Card" />
<div data-cc-digits>
<input size="4" type="text" placeholder="----">
<input size="4" type="text" placeholder="----">
<input size="4" type="text" placeholder="----">
<input size="4" type="text" placeholder="----">
</div>
<div data-cc-info>
<input size="20" type="text" placeholder="Name Surname">
<input size="6" class="second-input" type="text" placeholder="MM/YY">
</div>
</div>
</div>
<button class="mdc-button" data-pay-btn>
Pay & Checkout Now
</button>
<script>
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 appState = {};
const formatAsMoney = (amount, buyerCountry) => {
const countryDetails = countries.find(({country}) => {
return country === buyerCountry;
}) || countries[0]
return amount.toLocaleString((`en-${countryDetails.code}`), {
style: 'currency',
currency: countryDetails.currency
});
};
const flagIfInvalid = (field, isValid) => {
if(isValid === true) {
field.classList.remove('is-invalid');
field.classList.add('is-valid');
} else {
field.classList.add('is-invalid');
field.classList.remove('is-valid');
}
};
const expiryDateFormatIsValid = (target) => {
return /\d\d\/\d\d/.test(target);
}
const detectCardType = ({target}) => {
const $cardContainer = document.querySelector('[data-credit-card]');
const $cardType = document.querySelector('[data-card-type]');
let cardType = '';
if(target.value.startsWith('4')) {
$cardContainer.classList.add('is-visa');
$cardContainer.classList.remove('is-mastercard');
$cardType.src = supportedCards.visa;
cardType = 'is-visa';
} else {
$cardContainer.classList.remove('is-visa');
$cardContainer.classList.add('is-mastercard');
$cardType.src = supportedCards.mastercard;
cardType = 'is-mastercard';
}
return cardType;
};
const validateCardExpiryDate = ({target}) => {
const hasCorrectDateFormat = expiryDateFormatIsValid(target.value);
const isFutureDate = isInTheFuture(target.value);
const isValid = hasCorrectDateFormat && isFutureDate;
flagIfInvalid(target, isValid);
return isValid;
};
const isInTheFuture = (dateString) => {
const currentDate = new Date();
const currentCentury = currentDate.getFullYear() / 100 | 0 + '';
const dateDetails = dateString.split('/');
const targetMonthIndex = dateDetails[0];
const targetYear = currentCentury + dateDetails[1];
return currentDate < new Date(targetYear, targetMonthIndex, 1);
};
const validateCardHolderName = ({target}) => {
const isValid = /^([a-zA-Z]{3,})\s([a-zA-Z]{3,})$/.test(target.value);
flagIfInvalid(target, isValid);
return isValid
};
const validateWithLuhn = (digits) => {
if(digits.length > 16){
return false;
}
let isValid = true;
const reversedDigits = [...digits].reverse();
const lastDigit = reversedDigits[0];
const doubledDigits = reversedDigits.map((digit, index) => {
if(index % 2 !== 0) {
return digit *2
}
return digit;
});
// if a doubled # is > 9, replace it with 9 subtracted from it
const replacedDoubleDigits = doubledDigits.map((digit, index) => {
if((index % 2 !== 0) && digit > 9) {
return digit - 9;
}
return digit;
})
const sumOfDigits = replacedDoubleDigits.reduce((accumulator, digit) => accumulator + digit, 0);
isValid = sumOfDigits % 10 === 0;
return isValid;
}
const validateCardNumber = () => {
const $cardInputs = document.querySelectorAll('[data-cc-digits] input');
const cardDigits = Array.from($cardInputs).reduce((accumulator, $input) => {
return accumulator.concat($input.value.split(''));
}, []);
const cardDigitsAsNumbers = cardDigits.map((digit) => {
return parseInt(digit,10);
});
const isValidNumber = validateWithLuhn(cardDigitsAsNumbers);
const $digitsContainer = document.querySelector('[data-cc-digits]');
if(isValidNumber) {
$digitsContainer.classList.remove('is-invalid');
}
else {
$digitsContainer.classList.add('is-invalid');
}
return isValidNumber;
};
const uiCanInteract = () => {
document.querySelector('[data-cc-digits] input').addEventListener('blur', detectCardType);
document.querySelector('[data-cc-info] input').addEventListener('blur', validateCardHolderName);
document.querySelector('[data-cc-info] .second-input').addEventListener('blur', validateCardExpiryDate);
document.querySelector('[data-pay-btn]').addEventListener('click', validateCardNumber);
document.querySelector('[data-cc-digits] input').focus();
};
const startApp = () => {
fetchBill();
};
const displayCartTotal = ({results}) => {
const [data] = results;
const {itemsInCart, buyerCountry} = data;
appState.items = itemsInCart;
appState.country = buyerCountry;
appState.bill = itemsInCart.reduce((accumulator, {qty, price}) => {
return accumulator + (qty * price);
}, 0);
appState.billFormatted = formatAsMoney(appState.bill, appState.country);
$bill = document.querySelector('[data-bill]');
$bill.textContent = appState.billFormatted;
uiCanInteract();
}
const fetchBill = () => {
const api = 'https://randomapi.com/api/006b08a801d82d0c9824dcfdfdfa3b3c';
fetch(api).then( res => {
return res.json();
})
.then(displayCartTotal)
.catch(error => {
console.log(error.message);
});
}
startApp();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment