Show inline password requirements with realtime validation #229

Closed
priyanshu_bharti wants to merge 1 commits from priyanshu_bharti:224-show-password-requirement-on-signup into dev
4 changed files with 73 additions and 4 deletions
@@ -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}>
+7 -2
View File
@@ -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}>
+30
View File
@@ -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()}.`
}
}