Created
August 7, 2019 19:58
-
-
Save gfredtech/0f7360af1fc81aecbb462d6e0c3b86dc 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; | |
} | |
[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 */ | |
[data-cart-info] span { | |
display: inline-block; | |
vertical-align: middle; | |
} | |
.material-icons { | |
font-size: 150px; | |
} | |
[data-credit-card] { | |
width: 435px; | |
min-height: 240px; | |
border-radius: 10px; | |
background-color: #5d6874; | |
} | |
[data-card-type] { | |
display: block; | |
width: 120px; | |
height: 60px; | |
} | |
[data-cc-digits] { | |
margin-top: 2em; | |
} | |
[data-cc-digits] input[type="text"] { | |
color: white; | |
font-size: 2em; | |
line-height: 2em; | |
border: none; | |
background: none; | |
margin-right: 0.5em; | |
} | |
[data-cc-info] { | |
margin-top: 1em; | |
} | |
[data-cc-info] input[type="text"] { | |
color: white; | |
font-size: 1.2em; | |
border: none; | |
background: none; | |
} | |
[data-cc-info] input:last-child { | |
padding-right: 10px; | |
float: right; | |
} | |
[data-pay-btn] { | |
position: fixed; | |
width: 90%; | |
border: solid 1px; | |
bottom: 20px; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- your HTML goes here --> | |
<div data-cart-info=""> | |
<heading class="mdc-typography--headline4"> | |
<span class="material-icons">shopping_cart</span> | |
<span data-bill=""></span> | |
</heading> | |
</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 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" size="20" placeholder="Name Surname" /> | |
<input type="text" size="6" placeholder="MM/YY" /> | |
</div> | |
</div> | |
</div> | |
<button class="mdc-button" data-pay-btn=""> | |
Pay Now | |
</button> | |
<script> | |
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 formatAsMoney = (amount, buyerCountry) => { | |
const country = | |
countries.find(x => x.country == buyerCountry) || countries[0]; | |
return amount.toLocaleString(`en-${country.code}`, { | |
style: "currency", | |
currency: country.currency | |
}); | |
}; | |
const flagIfInvalid = (field, isValid) => { | |
field.classList.toggle("is-invalid", !isValid); | |
}; | |
const detectCardType = first4Digits => { | |
first4Digits = first4Digits.join(""); | |
const creditCard = document.querySelector("[data-credit-card]"); | |
const cardType = document.querySelector("[data-card-type]"); | |
creditCard.classList.remove("is-mastercard"); | |
creditCard.classList.remove("is-visa"); | |
if (first4Digits.startsWith("4")) { | |
creditCard.classList.add("is-visa"); | |
cardType.src = supportedCards.visa; | |
return "is-visa"; | |
} else if (first4Digits.startsWith("5")) { | |
creditCard.classList.add("is-mastercard"); | |
cardType.src = supportedCards.mastercard; | |
return "is-mastercard"; | |
} else cardType.src = "https://placehold.it/120x60.png?text=Card"; | |
}; | |
const expiryDateFormatIsValid = field => { | |
let re = /^[^\/]{0,2}\/[^\/]{2}$/; | |
if (!re.test(field)) return false; | |
const [mm, yy] = field.split("/").map(e => parseInt(e, 10)); | |
const cMM = new Date().getMonth(); | |
const cYY = parseInt( | |
new Date() | |
.getFullYear() | |
.toString() | |
.substr(-2), | |
10 | |
); | |
if (cYY >= yy) return false; | |
return mm >= cMM; | |
}; | |
const validateCardExpiryDate = () => { | |
const expiry = document.querySelector( | |
"[data-cc-info] input:nth-child(2)" | |
); | |
const result = expiryDateFormatIsValid(expiry.value); | |
flagIfInvalid(expiry, result); | |
return result; | |
}; | |
const validateCardHolderName = () => { | |
const name = document.querySelector( | |
"[data-cc-info] input:nth-child(1)" | |
); | |
let re = /^[^\s]{3,}\s[^\s]{3,}$/; | |
const result = re.test(name.value); | |
flagIfInvalid(name, result); | |
return result; | |
}; | |
const validateWithLuhn = digits => { | |
digits = digits.map(x => parseInt(x, 10)); | |
for (let i = digits.length - 2; i >= 0; i -= 2) { | |
digits[i] *= 2; | |
if (digits[i] > 9) { | |
digits[i] -= 9; | |
} | |
} | |
const sum = digits.reduce((total, x) => total + x, 0); | |
return sum % 10 == 0; | |
}; | |
const validateCardNumber = () => { | |
const digitsArray = appState.cardDigits.flat(); | |
const result = validateWithLuhn(digitsArray); | |
const digitDiv = document.querySelector("[data-cc-digits]"); | |
flagIfInvalid(digitDiv, result); | |
return result; | |
}; | |
const validatePayment = () => { | |
validateCardNumber(); | |
validateCardHolderName(); | |
validateCardExpiryDate(); | |
}; | |
const sleep = () => { | |
return new Promise(resolve => setTimeout(resolve, 500)); | |
}; | |
const smartInput = (event, fieldIndex, fields) => { | |
if (fieldIndex > 3) return; | |
const EventTypes = { | |
NUMBER: 1, | |
BACKSPACE: 2, | |
NONPRINT: 3, | |
OTHER: 4 | |
}; | |
const eventType = | |
event.key === "Backspace" | |
? EventTypes.BACKSPACE | |
: event.key.length > 1 | |
? EventTypes.NONPRINT | |
: event.key >= "0" && event.key <= "9" | |
? EventTypes.NUMBER | |
: EventTypes.OTHER; | |
switch (eventType) { | |
case EventTypes.OTHER: | |
event.preventDefault(); | |
return; | |
case EventTypes.BACKSPACE: | |
appState.cardDigits[fieldIndex].pop(); | |
return; | |
case EventTypes.NUMBER: | |
if (!appState.cardDigits[fieldIndex]) { | |
appState.cardDigits[fieldIndex] = [event.key]; | |
} else { | |
appState.cardDigits[fieldIndex].push(event.key); | |
} | |
const first4 = appState.cardDigits[0]; | |
detectCardType(first4); | |
if (fieldIndex != 3) { | |
const ccDigitsField = fields[fieldIndex]; | |
event.preventDefault(); | |
ccDigitsField.value += event.key; | |
sleep().then(() => { | |
ccDigitsField.value = "$".repeat( | |
appState.cardDigits[fieldIndex].length | |
); | |
}); | |
} | |
} | |
smartCursor(event, fieldIndex, fields); | |
}; | |
const smartCursor = (event, fieldIndex, fields) => { | |
const input = fields[fieldIndex].value; | |
const size = parseInt(fields[fieldIndex].getAttribute("size"), 10); | |
if (input.length >= size && fieldIndex != fields.length - 1) { | |
fields[fieldIndex + 1].focus(); | |
} | |
}; | |
const enableSmartTyping = () => { | |
const inputFields = document.querySelectorAll("input"); | |
inputFields.forEach((field, index, fields) => { | |
field.addEventListener("keydown", event => | |
smartInput(event, index, fields) | |
); | |
}); | |
}; | |
const uiCanInteract = () => { | |
const creditCardNumber = document.querySelector( | |
"div[data-cc-digits] input" | |
); | |
creditCardNumber.focus(); | |
const payBtn = document.querySelector("[data-pay-btn]"); | |
payBtn.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, { price, qty }) => total + qty * price, | |
0 | |
); | |
appState.billFormatted = formatAsMoney(appState.bill, appState.country); | |
const billSpan = document.querySelector("[data-bill]"); | |
billSpan.textContent = appState.billFormatted; | |
appState.cardDigits = []; | |
uiCanInteract(); | |
}; | |
const fetchBill = () => { | |
const apiHost = "https://randomapi.com/api"; | |
const apiKey = "006b08a801d82d0c9824dcfdfdfa3b3c"; | |
const apiEndpoint = `${apiHost}/${apiKey}`; | |
fetch(apiEndpoint) | |
.then(response => response.json()) | |
.then(data => displayCartTotal(data)) | |
.catch(error => console.log(error)); | |
}; | |
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