├── 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 |
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 |
--------------------------------------------------------------------------------