Last active
July 15, 2019 16:53
-
-
Save cziem/989e8fbcdbf697c735e76792b2f944e3 to your computer and use it in GitHub Desktop.
PayCard Functionality
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-eqX-UA-Compatible content="ie=edge" /> | |
| <title>Mini App</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 1em; | |
| background-color: #fff; | |
| } | |
| [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; | |
| } | |
| [data-cart-info] span { | |
| display: inline-block; | |
| vertical-align: middle; | |
| } | |
| .mdc-card__primary-action, | |
| .mdc-card__primary-action:hover { | |
| cursor: auto; | |
| padding: 20px; | |
| min-height: inherit; | |
| } | |
| [data-credit-card] { | |
| width: 435px; | |
| min-height: 240px; | |
| border-radius: 10px; | |
| background-color: #5d6874; | |
| } | |
| img[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] input:nth-child(2) { | |
| padding-right: 10px; | |
| float: right; | |
| } | |
| [data-credit-card] [data-card-type] { | |
| transition: width 1.5s; | |
| margin-left: calc(100% - 130px); | |
| } | |
| [data-pay-btn] { | |
| position: fixed; | |
| width: 90%; | |
| border: 1px solid; | |
| bottom: 20px; | |
| } | |
| .material-icons { | |
| font-size: 150px; | |
| } | |
| [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; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div data-cart-info> | |
| <h1 class="mdc-typography--headline4"> | |
| <span class="material-icons">shopping_cart</span> | |
| <span data-bill></span> | |
| </h1> | |
| </div> | |
| <div data-credit-card class="mdc-card mdc-card--outlined"> | |
| <div class="mdc-card__primary-action"> | |
| <img src="https://placehold.it/120x60.png?text=Card" data-card-type /> | |
| <div data-cc-digits> | |
| <input size="4" placeholder="----" type="text" /> | |
| <input size="4" placeholder="----" type="text" /> | |
| <input size="4" placeholder="----" type="text" /> | |
| <input size="4" placeholder="----" type="text" /> | |
| </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 & Checkout Now</button> | |
| <script> | |
| const appState = {} | |
| 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 formatAsMoney = (amount, buyerCountry) => { | |
| const country = countries | |
| .filter(data => data.country === buyerCountry) | |
| const { code, currency } = country[0]; | |
| if (!country) { | |
| return amount.toLocaleString("en-US", { style: 'currency', currency: "USD" }) | |
| } else { | |
| return amount.toLocaleString(`en-${code}`, { style: 'currency', currency }) | |
| } | |
| } | |
| const flagIfInvalid = (field, isValid) => { | |
| if (isValid) { | |
| field.classList.remove('is-invalid') | |
| } else { | |
| field.classList.add('is-invalid') | |
| } | |
| } | |
| const expiryDateFormatIsValid = (target) => { | |
| const regex = /^\d{2,2}\/\d{2,2}/; | |
| return regex.test(target) | |
| } | |
| const detectCardType = ({ target }) => { | |
| const card = document.querySelector('[data-credit-card]') | |
| const img = document.querySelector('[data-card-type]') | |
| if (target.value.startsWith('5')) { | |
| card.classList.add('is-mastercard'); | |
| card.classList.remove('is-visa'); | |
| img.src = supportedCards.mastercard; | |
| return 'is-mastercard'; | |
| } else if (target.value.startsWith('4')) { | |
| card.classList.add('is-visa'); | |
| card.classList.remove('is-mastercard'); | |
| img.src = supportedCards.visa; | |
| return 'is-visa'; | |
| } else { | |
| return 'not-supported'; | |
| } | |
| } | |
| const validateCardExpiryDate = ({ target }) => { | |
| const isInTheFuture = () => { | |
| const dateStr = target.value; | |
| const dateArr = dateStr.split("/"); | |
| const cardDate = new Date(2000 + Number.parseInt(dateArr[1]), Number.parseInt(dateArr[0] - 1)); | |
| const today = new Date(); | |
| return cardDate.getTime() > today.getTime(); | |
| } | |
| if (expiryDateFormatIsValid(target.value) && isInTheFuture()) { | |
| flagIfInvalid(target, true); | |
| return true; | |
| } else { | |
| flagIfInvalid(target, false) | |
| return false; | |
| } | |
| } | |
| const validateCardHolderName = ({ target }) => { | |
| const validNames = /^[\w]{3,}/; | |
| const names = target.value.split(' ') | |
| const [firstName, lastName] = names; | |
| if (validNames.test(firstName) && validNames.test(lastName) && names.length === 2) { | |
| flagIfInvalid(target, true); | |
| return true; | |
| } else { | |
| flagIfInvalid(target, false); | |
| return false; | |
| } | |
| } | |
| const validateWithLuhn = (digits) => { | |
| return digits.reduceRight((prev, curr, idx) => { | |
| prev = parseInt(prev, 10); | |
| if ((idx + 1) % 2 !== 0) { | |
| curr = (curr * 2).toString().split('').reduce((p, c) => parseInt(p, 10) + parseInt(c, 10)); | |
| } | |
| return prev + parseInt(curr, 10) | |
| }) % 10 === 0 | |
| } | |
| const validateCardNumber = () => { | |
| const cardDigits = document.querySelector('[data-cc-digits]') | |
| let cardNumber = ''; | |
| const cardInputs = Array.from(cardDigits.querySelectorAll('input')); | |
| cardInputs.map(({value}) => { | |
| if (value.length === 4) { | |
| return cardNumber += value; | |
| } | |
| }) | |
| const cardArray = [...cardNumber] | |
| const validCardNum = validateWithLuhn(cardArray) | |
| if (!validCardNum) { | |
| cardDigits.classList.add('is-invalid') | |
| return false | |
| } else if (validCardNum) { | |
| cardDigits.classList.remove('is-invalid') | |
| return true | |
| } | |
| } | |
| const uiCanInteract = () => { | |
| const ccDigits = document.querySelector('[data-cc-digits]') | |
| const inputs = ccDigits.querySelectorAll('input') | |
| inputs[0].addEventListener('blur', detectCardType) | |
| const info = document.querySelector('[data-cc-info]') | |
| const infoDetails = info.querySelectorAll('input') | |
| infoDetails[0].addEventListener('blur', validateCardHolderName) | |
| infoDetails[1].addEventListener('blur', validateCardExpiryDate) | |
| document.querySelector('[data-pay-btn]') | |
| .addEventListener('click', validateCardNumber) | |
| inputs[0].focus() | |
| } | |
| const displayCartTotal = ({ results }) => { | |
| const [ data ] = results; | |
| const { itemsInCart, buyerCountry } = data; | |
| appState.items = itemsInCart; | |
| appState.country = buyerCountry; | |
| appState.bill = itemsInCart.reduce((acc, cur) => acc + (cur.price * cur.qty), 0); | |
| appState.billFormatted = formatAsMoney(appState.bill, appState.country); | |
| document.querySelector('[data-bill]').textContent = appState.billFormatted; | |
| uiCanInteract(); | |
| } | |
| const fetchBill = () => { | |
| const api = "https://randomapi.com/api/006b08a801d82d0c9824dcfdfdfa3b3c" | |
| fetch(api) | |
| .then(response => response.json()) | |
| .then(data => displayCartTotal(data)) | |
| .catch(err => console.log("Error: ", 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