Created
August 16, 2019 13:50
-
-
Save jet2018/72751bbf25a49964905b2517561e5144 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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