Skip to content

Instantly share code, notes, and snippets.

@jet2018
Created August 16, 2019 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jet2018/72751bbf25a49964905b2517561e5144 to your computer and use it in GitHub Desktop.
Save jet2018/72751bbf25a49964905b2517561e5144 to your computer and use it in GitHub Desktop.
<!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;
}
div[data-cart-info] span {
display: inline-block;
vertical-align: middle;
}
span.material-icons {
font-size: 150px;
}
div[data-credit-card] {
width: 435px;
min-height: 240px;
border-radius: 10px;
background-color: #5d6874;
}
img[data-card-type] {
display: block;
width: 120px;
height: 60px;
}
div[data-cc-digits] {
margin-top: 2em;
}
div[data-cc-digits] input {
color: white;
font-size: 2em;
line-height: 2em;
border: none;
background: none;
margin-right: 0.5em;
}
div[data-cc-info] {
margin-top: 1em;
}
div[data-cc-info] input {
color: white;
font-size: 1.2em;
border: none;
background: none;
}
div[data-cc-info] input:nth-child(2) {
padding-right: 10px;
float: right;
}
button[data-pay-btn] {
position: fixed;
width: 90%;
border: 1px solid;
bottom: 20px;
}
[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;
}
/* Add Your CSS From Here */
</style>
</head>
<body>
<!-- your HTML goes here -->
<div data-cart-info>
<h1 class="mdc-typography--headline4"><span class="material-icons">shopping_cart</span><span data-bill></span></h1>
</div>
<div class="mdc-card mdc-card--outlined" data-credit-card>
<div class="mdc-card__primary-action">
<img data-card-type src="[http://placehold.it/120x60.png?text=Card](http://placehold.it/120x60.png?text=Card)">
<div data-cc-digits>
<input type="text" size="4" placeholder="----" />
<input type="text" size="4" placeholder="----" />
<input type="text" size="4" placeholder="----" />
<input type="text" size="4" placeholder="----" />
</div>
<div data-cc-info>
<input type="text" id="name" size="20" placeholder="Name Surname"/>
<input type="text" id="date" size="6" placeholder="MM/YY"/>
</div>
</div>
</div>
<button class="mdc-button" data-pay-btn>Pay Now</button>
<script>
const formatAsMoney = (amount, buyerCountry) => {
const isInCountries = countries.find(country => country.country == buyerCountry);
const locale = isInCountries ? isInCountries : countries[0];
return amount.toLocaleString(`en-${locale.code}`, {style: "currency", currency: locale.currency});
};
const flagIfInvalid = (field, isValid) => {
isValid ? field.classList.remove('is-invalid') : field.classList.add('is-invalid');
};
const expiryDateFormatIsValid = (field) => {
const regex = /^[\d]{1,}\/[\d]{2}$/;
return regex.test(field.value);
};
const smartCursor = (event, fieldIndex, fields) => {
const size = fields[fieldIndex].size;
fields[fieldIndex].value.length == size && fieldIndex < fields.length ? fields[fieldIndex+1].focus() : false ;
};
const enableSmartTyping = () => {
const inputArray = [...document.querySelectorAll("input")];
inputArray.forEach((field, index, fields) => {
field.addEventListener('keyup', event => smartCursor(event, index, fields));
field.addEventListener('keydown', event => smartInput(event, index, fields));
});
};
const detectCardType = (first4Digits) => {
const cardDiv = document.querySelector("div[data-credit-card]");
const cardImg = document.querySelector("img[data-card-type]");
let cardType, img;
first4Digits[0] == "5" ? cardType = "mastercard" : cardType = "visa";
first4Digits[0] == "4" ? img = supportedCards.visa : img = supportedCards.mastercard ;
if(cardDiv.classList.contains("is-mastercard") || cardDiv.classList.contains("is-visa")) { cardDiv.classList.remove('is-mastercard', 'is-visa')};
cardImg.src = img;
cardDiv.classList.add(`is-${cardType}`);
return `is-${cardType}`;
};
const validateCardExpiryDate = () => {
const date = document.querySelector('#date');
const isValid = expiryDateFormatIsValid(date);
const expMonth = date.value.split('/')[0];
const expYear = `20${date.value.split('/')[1]}`;
const userDate = new Date(`${expMonth}-01-${expYear}`);
const result = isValid && userDate >= new Date() ? true: false;
flagIfInvalid(date, result);
return result;
};
const validateCardHolderName = () => {
const name = document.querySelector('#name');
const isValid = /^([a-zA-Z]{3,})\s([a-zA-Z]{3,})$/.test(name.value);
flagIfInvalid(name, isValid);
return isValid;
};
const smartInput = (event, fieldIndex, fields) => {
let hashes = '';
let exceptions = ['Backspace','Delete','Tab','Shift','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'];
if(fieldIndex < 4 && /^[\d]/.test(event.key)){
appState.cardDigits[fieldIndex] === undefined ? appState.cardDigits[fieldIndex] = [] : appState.cardDigits[fieldIndex].push(parseInt(event.key));
appState.cardDigits[fieldIndex].length === 0 ? appState.cardDigits[fieldIndex].push(parseInt(event.key)) : '';
setTimeout(() => {
hashes="#".repeat(appState.cardDigits[fieldIndex].length);
fieldIndex < 3 ? event.target.value = hashes : false ;
}, 500);
const first4Digits = appState.cardDigits[0];
detectCardType(first4Digits);
}
};
const validateWithLuhn = (digits) => {
let sum, nine, double;
let array = digits.map(x => parseInt(x)).reverse();
array.forEach((num, index, array) => {
if(index % 2 == 1) {
double = num * 2;
nine = double > 9 ? double - 9: double;
array[index] = nine;
}
})
sum = array.reduce((a,b) => a + b, 0);
let isValid = sum % 10 === 0 ? true : false;
return isValid;
};
const validateCardNumber = () => {
const is16Digits = appState.cardDigits.length == 16 ? appState.cardDigits.flat() : false;
const isLuhnValid = validateWithLuhn(is16Digits);
flagIfInvalid(document.querySelector('[data-cc-digits]'), isLuhnValid);
return isLuhnValid;
};
const validatePayment = () => {
validateCardNumber();
validateCardHolderName();
validateCardExpiryDate();
};
const uiCanInteract = () => {
document.querySelector('div[data-cc-digits] input:nth-child(1)').focus();
document.querySelector('button[data-pay-btn]').addEventListener('click', validatePayment);
billHype();
enableSmartTyping();
};
const displayCartTotal = ({results}) => {
const [data] = results;
const {itemsInCart, buyerCountry} = data;
appState.items = itemsInCart;
appState.country = buyerCountry;
appState.bill = itemsInCart.reduce((total, item) => total + (item.qty * item.price), 0);
appState.billFormatted = formatAsMoney(appState.bill, appState.country);
document.querySelector('span[data-bill]').textContent = appState.billFormatted;
appState.cardDigits = [];
uiCanInteract();
};
const supportedCards = {
visa, mastercard
};
const countries = [
{
code: "US",
currency: "USD",
currencyName: '',
country: 'United States'
},
{
code: "NG",
currency: "NGN",
currencyName: '',
country: 'Nigeria'
},
{
code: 'KE',
currency: 'KES',
currencyName: '',
country: 'Kenya'
},
{
code: 'UG',
currency: 'UGX',
currencyName: '',
country: 'Uganda'
},
{
code: 'RW',
currency: 'RWF',
currencyName: '',
country: 'Rwanda'
},
{
code: 'TZ',
currency: 'TZS',
currencyName: '',
country: 'Tanzania'
},
{
code: 'ZA',
currency: 'ZAR',
currencyName: '',
country: 'South Africa'
},
{
code: 'CM',
currency: 'XAF',
currencyName: '',
country: 'Cameroon'
},
{
code: 'GH',
currency: 'GHS',
currencyName: '',
country: 'Ghana'
}
];
const billHype = () => {
const billDisplay = document.querySelector('.mdc-typography--headline4');
if (!billDisplay) return;
billDisplay.addEventListener('click', () => {
const billSpan = document.querySelector("[data-bill]");
if (billSpan &&
appState.bill &&
appState.billFormatted &&
appState.billFormatted === billSpan.textContent) {
window.speechSynthesis.speak(
new SpeechSynthesisUtterance(appState.billFormatted)
);
}
});
};
const appState = {};
const fetchBill = (whatever) => {
const apiHost = 'https://randomapi.com/api';
const apiKey = '006b08a801d82d0c9824dcfdfdfa3b3c';
const apiEndpoint = `${apiHost}/${apiKey}`;
fetch(apiEndpoint)
.then(response => response.json())
.then(data => displayCartTotal(data))
.catch(err => console.warn(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