├── LICENSE ├── visa.svg ├── index.html ├── styles.css ├── mastercard.svg └── script.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 WebDevSimplified 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /visa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Document 10 | 11 | 12 |
13 |
14 |
15 |
WDS Bank
16 | 17 |
18 |
19 | Card Number 20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 | Expiration 35 | 36 |
37 | 51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 |
62 |
63 |
64 | 65 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .credit-card { 2 | color: white; 3 | font-family: Arial; 4 | position: relative; 5 | } 6 | 7 | .credit-card .front, 8 | .credit-card .back { 9 | background-color: hsl(200, 80%, 30%); 10 | border: 1px solid hsl(200, 80%, 10%); 11 | border-radius: .5rem; 12 | width: 325px; 13 | height: 150px; 14 | padding: .75rem 1rem; 15 | padding-bottom: 1.25rem; 16 | } 17 | 18 | .credit-card .front { 19 | display: flex; 20 | flex-direction: column; 21 | gap: 1rem; 22 | z-index: 1; 23 | overflow: hidden; 24 | position: relative; 25 | } 26 | 27 | .credit-card .card-data-row { 28 | display: flex; 29 | margin-bottom: auto; 30 | } 31 | 32 | .credit-card .logo { 33 | height: 40px; 34 | width: 50px; 35 | flex-grow: 0; 36 | } 37 | 38 | .credit-card .brand-name { 39 | flex-grow: 1; 40 | font-size: 1.25rem; 41 | font-weight: bold; 42 | } 43 | 44 | .credit-card .form-group { 45 | display: flex; 46 | flex-direction: column; 47 | gap: .25rem; 48 | } 49 | 50 | .credit-card fieldset { 51 | border: none; 52 | padding: 0; 53 | margin: 0; 54 | } 55 | 56 | .credit-card fieldset legend { 57 | visibility: hidden; 58 | height: 0; 59 | width: 0; 60 | position: absolute; 61 | top: -200vh; 62 | } 63 | 64 | .credit-card .horizontal-input-stack { 65 | display: flex; 66 | gap: .5rem; 67 | } 68 | 69 | .credit-card .cc-inputs input { 70 | width: 4ch; 71 | font-family: monospace; 72 | } 73 | 74 | .credit-card input, 75 | .credit-card select { 76 | padding: .25em .5em; 77 | border: none; 78 | border-radius: .25em; 79 | appearance: none; 80 | } 81 | 82 | .credit-card label { 83 | font-size: .65rem; 84 | text-transform: uppercase; 85 | } 86 | 87 | .credit-card .input-row { 88 | display: flex; 89 | gap: 2rem; 90 | } 91 | 92 | .credit-card .name-group { 93 | flex-grow: 1; 94 | } 95 | 96 | .credit-card .front::before { 97 | content: ""; 98 | position: absolute; 99 | height: 400px; 100 | width: 400px; 101 | border-radius: 100%; 102 | background-color: hsl(0, 0%, 100%, .15); 103 | top: -250px; 104 | left: -150px; 105 | z-index: -1; 106 | } 107 | 108 | .credit-card .front::after { 109 | content: ""; 110 | position: absolute; 111 | height: 600px; 112 | width: 600px; 113 | border-radius: 100%; 114 | background-color: hsl(0, 0%, 100%, .075); 115 | bottom: -475px; 116 | left: -150px; 117 | z-index: -1; 118 | } 119 | 120 | .credit-card .back { 121 | position: absolute; 122 | top: 2rem; 123 | left: 3.25rem; 124 | } 125 | 126 | .credit-card .stripe { 127 | background-color: hsl(200, 80%, 10%); 128 | height: 35px; 129 | position: absolute; 130 | left: 0; 131 | right: 0; 132 | top: 1.5rem; 133 | } 134 | 135 | .credit-card .cvc-group { 136 | position: absolute; 137 | bottom: 3.25rem; 138 | right: .5rem; 139 | } 140 | 141 | .credit-card .cvc-input { 142 | width: 3ch; 143 | font-family: monospace; 144 | } -------------------------------------------------------------------------------- /mastercard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const expirationSelect = document.querySelector("[data-expiration-year]") 2 | const logo = document.querySelector("[data-logo]") 3 | 4 | const currentYear = new Date().getFullYear() 5 | for (let i = currentYear; i < currentYear + 10; i++) { 6 | const option = document.createElement("option") 7 | option.value = i 8 | option.innerText = i 9 | expirationSelect.append(option) 10 | } 11 | 12 | document.addEventListener("keydown", e => { 13 | const input = e.target 14 | const key = e.key 15 | if (!isConnectedInput(input)) return 16 | 17 | switch (key) { 18 | case "ArrowLeft": { 19 | if (input.selectionStart === 0 && input.selectionEnd === 0) { 20 | const prev = input.previousElementSibling 21 | prev.focus() 22 | prev.selectionStart = prev.value.length - 1 23 | prev.selectionEnd = prev.value.length - 1 24 | e.preventDefault() 25 | } 26 | break 27 | } 28 | case "ArrowRight": { 29 | if ( 30 | input.selectionStart === input.value.length && 31 | input.selectionEnd === input.value.length 32 | ) { 33 | const next = input.nextElementSibling 34 | next.focus() 35 | next.selectionStart = 1 36 | next.selectionEnd = 1 37 | e.preventDefault() 38 | } 39 | break 40 | } 41 | case "Delete": { 42 | if ( 43 | input.selectionStart === input.value.length && 44 | input.selectionEnd === input.value.length 45 | ) { 46 | const next = input.nextElementSibling 47 | next.value = next.value.substring(1, next.value.length) 48 | next.focus() 49 | next.selectionStart = 0 50 | next.selectionEnd = 0 51 | e.preventDefault() 52 | } 53 | break 54 | } 55 | case "Backspace": { 56 | if (input.selectionStart === 0 && input.selectionEnd === 0) { 57 | const prev = input.previousElementSibling 58 | prev.value = prev.value.substring(0, prev.value.length - 1) 59 | prev.focus() 60 | prev.selectionStart = prev.value.length 61 | prev.selectionEnd = prev.value.length 62 | e.preventDefault() 63 | } 64 | break 65 | } 66 | default: { 67 | if (e.ctrlKey || e.altKey) return 68 | if (key.length > 1) return 69 | if (key.match(/^[^0-9]$/)) return e.preventDefault() 70 | 71 | e.preventDefault() 72 | onInputChange(input, key) 73 | } 74 | } 75 | }) 76 | 77 | document.addEventListener("paste", e => { 78 | const input = e.target 79 | const data = e.clipboardData.getData("text") 80 | 81 | if (!isConnectedInput(input)) return 82 | if (!data.match(/^[0-9]+$/)) return e.preventDefault() 83 | 84 | e.preventDefault() 85 | onInputChange(input, data) 86 | }) 87 | 88 | function onInputChange(input, newValue) { 89 | const start = input.selectionStart 90 | const end = input.selectionEnd 91 | updateInputValue(input, newValue, start, end) 92 | focusInput(input, newValue.length + start) 93 | const firstFour = input 94 | .closest("[data-connected-inputs]") 95 | .querySelector("input").value 96 | 97 | if (firstFour.startsWith("4")) { 98 | logo.src = "visa.svg" 99 | } else if (firstFour.startsWith("5")) { 100 | logo.src = "mastercard.svg" 101 | } 102 | } 103 | 104 | function updateInputValue(input, extraValue, start = 0, end = 0) { 105 | const newValue = `${input.value.substring( 106 | 0, 107 | start 108 | )}${extraValue}${input.value.substring(end, 4)}` 109 | input.value = newValue.substring(0, 4) 110 | if (newValue > 4) { 111 | const next = input.nextElementSibling 112 | if (next == null) return 113 | updateInputValue(next, newValue.substring(4)) 114 | } 115 | } 116 | 117 | function focusInput(input, dataLength) { 118 | let addedChars = dataLength 119 | let currentInput = input 120 | while (addedChars > 4 && currentInput.nextElementSibling != null) { 121 | addedChars -= 4 122 | currentInput = currentInput.nextElementSibling 123 | } 124 | if (addedChars > 4) addedChars = 4 125 | 126 | currentInput.focus() 127 | currentInput.selectionStart = addedChars 128 | currentInput.selectionEnd = addedChars 129 | } 130 | 131 | function isConnectedInput(input) { 132 | const parent = input.closest("[data-connected-inputs]") 133 | return input.matches("input") && parent != null 134 | } 135 | --------------------------------------------------------------------------------