Skip to content

Instantly share code, notes, and snippets.

@gfredtech
Created August 7, 2019 19:58
Show Gist options
  • Save gfredtech/0f7360af1fc81aecbb462d6e0c3b86dc to your computer and use it in GitHub Desktop.
Save gfredtech/0f7360af1fc81aecbb462d6e0c3b86dc 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;
}
[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