Show inline password requirements with realtime validation #229
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {getPasswordRequirements} from "@app/util/password"
|
||||
|
||||
type Props = {
|
||||
password: string
|
||||
}
|
||||
|
||||
const {password}: Props = $props()
|
||||
|
||||
const requirements = $derived(getPasswordRequirements(password))
|
||||
</script>
|
||||
|
||||
<div class="mt-1 flex flex-col gap-1 text-xs">
|
||||
<p class="font-medium opacity-70">Password requirements</p>
|
||||
<ul class="flex flex-col gap-1 opacity-80">
|
||||
{#each requirements as requirement (requirement.key)}
|
||||
<li class="flex items-center gap-1.5">
|
||||
<Icon
|
||||
icon={requirement.met ? CheckCircle : CloseCircle}
|
||||
class={requirement.met ? "text-success" : "text-error"}
|
||||
size={3} />
|
||||
<span>{requirement.label}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -16,6 +16,8 @@
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import PasswordRequirements from "@app/components/PasswordRequirements.svelte"
|
||||
import {getPasswordValidationError} from "@app/util/password"
|
||||
import {loginWithPomade, deleteCurrentPomadeSession} from "@app/util/pomade"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -31,10 +33,12 @@
|
||||
const back = () => history.back()
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (password.trim().length < 12) {
|
||||
const passwordError = getPasswordValidationError(password)
|
||||
|
||||
if (passwordError) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Password must be at least 12 characters long.",
|
||||
message: passwordError,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,6 +108,7 @@
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<PasswordRequirements {password} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import PasswordRequirements from "@app/components/PasswordRequirements.svelte"
|
||||
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||
import {getPasswordValidationError} from "@app/util/password"
|
||||
import {pushToast, popToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
@@ -30,10 +32,12 @@
|
||||
const back = () => history.back()
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (password.trim().length < 12) {
|
||||
const passwordError = getPasswordValidationError(password)
|
||||
|
||||
if (passwordError) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Password must be at least 12 characters long.",
|
||||
message: passwordError,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,6 +139,7 @@
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<PasswordRequirements {password} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const DEFAULT_MIN_PASSWORD_LENGTH = 12
|
||||
|
||||
const parsedMinPasswordLength = Number.parseInt(import.meta.env.VITE_PASSWORD_MIN_LENGTH || "", 10)
|
||||
|
||||
export const MIN_PASSWORD_LENGTH =
|
||||
Number.isFinite(parsedMinPasswordLength) && parsedMinPasswordLength > 0
|
||||
? parsedMinPasswordLength
|
||||
: DEFAULT_MIN_PASSWORD_LENGTH
|
||||
|
||||
export type PasswordRequirement = {
|
||||
key: string
|
||||
label: string
|
||||
met: boolean
|
||||
}
|
||||
|
||||
export const getPasswordRequirements = (password: string): PasswordRequirement[] => [
|
||||
{
|
||||
key: "minLength",
|
||||
label: `At least ${MIN_PASSWORD_LENGTH} characters`,
|
||||
met: password.trim().length >= MIN_PASSWORD_LENGTH,
|
||||
},
|
||||
]
|
||||
|
||||
export const getPasswordValidationError = (password: string) => {
|
||||
const failed = getPasswordRequirements(password).find(({met}) => !met)
|
||||
|
||||
if (failed) {
|
||||
return `Password must be ${failed.label.toLowerCase()}.`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user