Changed 2FA page to input 6 characters and auto submit.
This commit is contained in:
parent
18a14bbd01
commit
44d7253367
@ -11,29 +11,120 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="edit-panel">
|
<section class="edit-panel two-factor-card">
|
||||||
<form asp-action="TwoFactor" method="post" class="asset-create-form">
|
<form asp-action="TwoFactor" method="post" class="asset-create-form two-factor-body" data-two-factor-form>
|
||||||
<input asp-for="ReturnUrl" type="hidden" />
|
<input asp-for="ReturnUrl" type="hidden" />
|
||||||
|
<input asp-for="Code" type="hidden" data-two-factor-code />
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="two-factor-code-group">
|
||||||
<div class="col-md-6">
|
<label class="form-label two-factor-label" for="twoFactorDigit1">Authenticator code</label>
|
||||||
<label class="form-label" asp-for="Code">Authenticator code</label>
|
<div class="two-factor-inputs" role="group" aria-label="Authenticator code">
|
||||||
<input class="form-control" asp-for="Code" autocomplete="one-time-code" />
|
@for (var i = 1; i <= 6; i++)
|
||||||
|
{
|
||||||
|
<input class="form-control two-factor-digit"
|
||||||
|
id="twoFactorDigit@(i)"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
maxlength="1"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
aria-label="Digit @i of 6"
|
||||||
|
data-two-factor-digit />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<span class="text-danger" asp-validation-for="Code"></span>
|
<span class="text-danger" asp-validation-for="Code"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-row mt-3">
|
<div class="two-factor-actions">
|
||||||
<button class="btn btn-primary" type="submit">Continue</button>
|
<button class="btn btn-primary" type="submit">Continue</button>
|
||||||
<a class="btn btn-outline-secondary" asp-action="Login">Back to Login</a>
|
<a class="btn btn-outline-secondary" asp-action="Login">Back to Login</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 mb-0">
|
<p class="two-factor-help">
|
||||||
<a asp-action="TwoFactorRecovery" asp-route-returnUrl="@Model.ReturnUrl">Can't access your authenticator app? Use a recovery code.</a>
|
<span>Can't access your authenticator app?</span>
|
||||||
|
<a asp-action="TwoFactorRecovery" asp-route-returnUrl="@Model.ReturnUrl">Use a recovery code.</a>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<partial name="_ValidationScriptsPartial" />
|
<partial name="_ValidationScriptsPartial" />
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const form = document.querySelector("[data-two-factor-form]");
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenCode = form.querySelector("[data-two-factor-code]");
|
||||||
|
const digits = Array.from(form.querySelectorAll("[data-two-factor-digit]"));
|
||||||
|
let hasSubmitted = false;
|
||||||
|
|
||||||
|
const codeValue = () => digits.map(input => input.value).join("");
|
||||||
|
const syncCode = () => {
|
||||||
|
hiddenCode.value = codeValue();
|
||||||
|
};
|
||||||
|
const isComplete = () => digits.every(input => input.value.length === 1);
|
||||||
|
const submitIfComplete = () => {
|
||||||
|
syncCode();
|
||||||
|
if (isComplete() && !hasSubmitted) {
|
||||||
|
hasSubmitted = true;
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const fillFrom = (startIndex, value) => {
|
||||||
|
const numbers = value.replace(/\D/g, "").slice(0, digits.length - startIndex);
|
||||||
|
numbers.split("").forEach((number, offset) => {
|
||||||
|
digits[startIndex + offset].value = number;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextIndex = Math.min(startIndex + numbers.length, digits.length - 1);
|
||||||
|
digits[nextIndex].focus();
|
||||||
|
submitIfComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
digits.forEach((input, index) => {
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
if (input.value.length > 1) {
|
||||||
|
fillFrom(index, input.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = input.value.replace(/\D/g, "");
|
||||||
|
syncCode();
|
||||||
|
|
||||||
|
if (input.value && index < digits.length - 1) {
|
||||||
|
digits[index + 1].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitIfComplete();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("keydown", event => {
|
||||||
|
if (event.key === "Backspace" && !input.value && index > 0) {
|
||||||
|
digits[index - 1].focus();
|
||||||
|
digits[index - 1].value = "";
|
||||||
|
syncCode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("paste", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
fillFrom(index, event.clipboardData.getData("text"));
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("focus", () => input.select());
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener("submit", () => {
|
||||||
|
syncCode();
|
||||||
|
});
|
||||||
|
|
||||||
|
digits.forEach(input => {
|
||||||
|
input.value = "";
|
||||||
|
});
|
||||||
|
syncCode();
|
||||||
|
digits[0].focus();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -205,7 +205,7 @@
|
|||||||
<div class="marketing-container">
|
<div class="marketing-container">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<p class="marketing-eyebrow">Loved by story-obsessed writers</p>
|
<p class="marketing-eyebrow">Loved by story-obsessed writers</p>
|
||||||
<h2>Authors use PlotDirector to feel oriented again.</h2>
|
<h2>Authors use PlotDirector to feel in control again.</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="testimonial-grid">
|
<div class="testimonial-grid">
|
||||||
<figure>
|
<figure>
|
||||||
|
|||||||
@ -896,6 +896,121 @@ a:focus-visible {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.two-factor-card {
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
border-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-body .alert {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-code-group {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--plotline-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-inputs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-digit {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 760;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border-color: rgba(122, 77, 44, 0.28);
|
||||||
|
background: var(--plotline-surface-elevated);
|
||||||
|
color: var(--plotline-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-digit:focus {
|
||||||
|
border-color: var(--plotline-accent);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 0.22rem rgba(122, 77, 44, 0.2),
|
||||||
|
0 10px 22px rgba(62, 45, 28, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-actions .btn {
|
||||||
|
min-width: 8.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-help {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--plotline-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-help a {
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-code-group .text-danger {
|
||||||
|
min-height: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.two-factor-card {
|
||||||
|
padding: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-inputs {
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-digit {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-factor-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.plotline-confirm-modal {
|
.plotline-confirm-modal {
|
||||||
background: #fffaf2;
|
background: #fffaf2;
|
||||||
border: 1px solid rgba(84, 68, 48, 0.16);
|
border: 1px solid rgba(84, 68, 48, 0.16);
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user