summaryrefslogtreecommitdiff
path: root/website
diff options
context:
space:
mode:
Diffstat (limited to 'website')
-rw-r--r--website/public/css/otp.css29
-rw-r--r--website/src/components/OtpDialog.astro31
-rw-r--r--website/src/scripts/otp-form-wc.ts68
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);