diff options
Diffstat (limited to 'website')
| -rw-r--r-- | website/public/css/otp.css | 29 | ||||
| -rw-r--r-- | website/src/components/OtpDialog.astro | 31 | ||||
| -rw-r--r-- | website/src/scripts/otp-form-wc.ts | 68 |
3 files changed, 128 insertions, 0 deletions
diff --git a/website/public/css/otp.css b/website/public/css/otp.css new file mode 100644 index 0000000..4a7db02 --- /dev/null +++ b/website/public/css/otp.css @@ -0,0 +1,29 @@ +.otp-inputs { + display: flex; + flex-direction: row; + gap: var(--spacing-inline-sm); + justify-content: center; + + input { + border: none; + border-block-end: 2px solid var(--colour-grey-fg); + font-size: var(--font-size-lg); + text-align: center; + width: var(--font-size-lg); + } +} + +.otp-form { + align-items: center; + display: flex; + flex-direction: column; + gap: var(--spacing-block-sm); + + > * { + margin: 0; + } + + input[type="submit"] { + max-width: max-content; + } +} diff --git a/website/src/components/OtpDialog.astro b/website/src/components/OtpDialog.astro new file mode 100644 index 0000000..1fd5fe9 --- /dev/null +++ b/website/src/components/OtpDialog.astro @@ -0,0 +1,31 @@ +--- +--- + +<script src="../scripts/otp-form-wc.ts"></script> +<link rel="stylesheet" href="/css/otp.css" /> + +<dialog class="otp-dialog"> + <otp-form> + <form class="otp-form" class="otp-form"> + <p> + I've sent a six-digit code to <span class="otp-recipient">your email address</span>. + Let me know what it is, so I can confirm this really is your email address. The + code will be valid <span class="otp-valid-until">for five minutes</span>. + </p> + + <p hidden class="error"/> + + <div class="otp-inputs"> + <input maxlength="1" name="1" required autocapitalize="characters" autocomplete="one-time-code" aria-label="First digit of one-time passcode" autofocus> + <input maxlength="1" name="2" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Second digit of one-time passcode"> + <input maxlength="1" name="3" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Third digit of one-time passcode"> + <input maxlength="1" name="4" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fourth digit of one-time passcode"> + <input maxlength="1" name="5" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fifth digit of one-time passcode"> + <input maxlength="1" name="6" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Sixth digit of one-time passcode"> + </div> + + <input type="submit" value="Verify"> + <button disabled class="resend-button">Resend (60s)</button> + </form> + </otp-form> +</dialog> diff --git a/website/src/scripts/otp-form-wc.ts b/website/src/scripts/otp-form-wc.ts new file mode 100644 index 0000000..5dd379e --- /dev/null +++ b/website/src/scripts/otp-form-wc.ts @@ -0,0 +1,68 @@ +class OtpForm extends HTMLElement { + observer?: MutationObserver; + + constructor() { + super(); + } + + connectedCallback() { + this.observer = new MutationObserver(() => { + this.clearInputs(); + this.configureInputs(); + }); + this.observer.observe(this, { childList: true, subtree: true }); + } + + disconnectedCallback() { + this.observer?.disconnect(); + } + + clearInputs() { + console.log("clearing all inputs"); + const inputs = this.querySelectorAll( + 'input:not([type="submit"])', + ) as NodeListOf<HTMLInputElement>; + for (const input of inputs) { + input.value = ""; + } + } + + configureInputs() { + this.observer?.disconnect(); + const inputs = this.querySelectorAll( + 'input:not([type="submit"])', + ) as NodeListOf<HTMLInputElement>; + const form = this.querySelector("form") as HTMLFormElement; + + for (const input of inputs) { + input.addEventListener("focus", () => { + input.select(); + }); + + input.addEventListener("input", () => { + if (input.value.length > 0) { + input.value = input.value.slice(0, 1).toLocaleUpperCase(); + const nextInput = input.nextElementSibling as HTMLInputElement; + if (nextInput) { + nextInput.focus(); + return; + } + + const submitButton = form.querySelector( + 'input[type="submit"]', + ) as HTMLInputElement; + submitButton.focus(); + let areAllInputsEntered = true; + inputs.forEach((input) => { + areAllInputsEntered = areAllInputsEntered && input.value.length > 0; + }); + if (areAllInputsEntered) { + form.requestSubmit(submitButton); + } + } + }); + } + } +} + +customElements.define("otp-form", OtpForm); |
