Add password requirements and password validation (#412)

* resolved

* made changes

* made submit button disabled on failing password checks

* removed submitted state and fixed typos

* redactored IsPasswordValid Component

* removed cursor-pointer from XCircleIcon

* added PasswordInput component

* made some refactors in PasswordInput component

* made eye icon lighter

* removed unsed code

* fix infinite state updates

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Dhruwang Jariwala
2023-06-26 18:13:50 +05:30
committed by GitHub
parent f1cc434e49
commit 38021d2026
5 changed files with 134 additions and 9 deletions

View File

@@ -0,0 +1,72 @@
import { CheckIcon } from "@heroicons/react/24/solid";
import React, { useState, useEffect } from "react";
interface Validation {
label: string;
state: boolean;
}
const PASSWORD_REGEX = {
UPPER_AND_LOWER: /^(?=.*[A-Z])(?=.*[a-z])/,
NUMBER: /\d/,
};
const DEFAULT_VALIDATIONS = [
{ label: "Mix of uppercase and lowercase", state: false },
{ label: "Minimum 8 characters long", state: false },
{ label: "Contain at least 1 number", state: false },
];
export default function IsPasswordValid({
password,
setIsValid,
}: {
password: string | null;
setIsValid: (isValid: boolean) => void;
}) {
const [validations, setValidations] = useState<Validation[]>(DEFAULT_VALIDATIONS);
useEffect(() => {
let newValidations = [...DEFAULT_VALIDATIONS];
if (password) {
newValidations = checkValidation(newValidations, 0, PASSWORD_REGEX.UPPER_AND_LOWER.test(password));
newValidations = checkValidation(newValidations, 1, password.length >= 8);
newValidations = checkValidation(newValidations, 2, PASSWORD_REGEX.NUMBER.test(password));
}
setIsValid(newValidations.every((validation) => validation.state === true));
setValidations(newValidations);
function checkValidation(prevValidations, index: number, state: boolean) {
const updatedValidations = [...prevValidations];
updatedValidations[index].state = state;
return updatedValidations;
}
}, [password, setIsValid]);
const renderIcon = (state: boolean) => {
if (state === false) {
return (
<span className="flex h-5 w-5 items-center justify-center">
<i className="inline-block h-2 w-2 rounded-full bg-slate-700"></i>
</span>
);
} else {
return <CheckIcon className="h-5 w-5" />;
}
};
return (
<div className="my-2 text-left text-slate-700 sm:text-sm">
<ul>
{validations.map((validation, index) => (
<li key={index}>
<div className="flex items-center">
{renderIcon(validation.state)}
{validation.label}
</div>
</li>
))}
</ul>
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { GoogleButton } from "@/components/auth/GoogleButton";
import { Button } from "@formbricks/ui";
import { Button, PasswordInput } from "@formbricks/ui";
import { XCircleIcon } from "@heroicons/react/24/solid";
import { signIn } from "next-auth/react";
import Link from "next/dist/client/link";
@@ -64,10 +64,9 @@ export const SigninForm = () => {
<label htmlFor="password" className="sr-only">
Password
</label>
<input
<PasswordInput
id="password"
name="password"
type="password"
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"

View File

@@ -1,6 +1,7 @@
"use client";
import { Button } from "@formbricks/ui";
import { PasswordInput } from "@formbricks/ui";
import { createUser } from "@/lib/users/users";
import { XCircleIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
@@ -8,6 +9,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useRef, useState } from "react";
import { GithubButton } from "./GithubButton";
import { GoogleButton } from "@/components/auth/GoogleButton";
import IsPasswordValid from "@/components/auth/IsPasswordValid";
export const SignupForm = () => {
const searchParams = useSearchParams();
@@ -17,8 +19,12 @@ export const SignupForm = () => {
const nameRef = useRef<HTMLInputElement>(null);
const handleSubmit = async (e: any) => {
setSigningUp(true);
e.preventDefault();
if(!isValid){
return
}
setSigningUp(true);
try {
await createUser(
e.target.elements.name.value,
@@ -42,6 +48,8 @@ export const SignupForm = () => {
const [isButtonEnabled, setButtonEnabled] = useState(true);
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const [password, setPassword] = useState<string|null>(null)
const [isValid, setIsValid] = useState(false)
const checkFormValidity = () => {
// If all fields are filled, enable the button
@@ -110,16 +118,17 @@ export const SignupForm = () => {
<label htmlFor="password" className="sr-only">
Password
</label>
<input
<PasswordInput
id="password"
name="password"
type="password"
value={password ? password : ""}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
placeholder="*******"
aria-placeholder="password"
onFocus={() => setIsPasswordFocused(true)}
required
className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
className="focus:border-brand focus:ring-brand block w-full rounded-md shadow-sm sm:text-sm"
/>
</div>
{process.env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && (
@@ -131,10 +140,12 @@ export const SignupForm = () => {
</Link>
</div>
)}
<IsPasswordValid password={password} setIsValid={setIsValid} />
</div>
)}
<Button
onClick={() => {
onClick={(e: any) => {
e.preventDefault()
if (!showLogin) {
setShowLogin(true);
setButtonEnabled(false);
@@ -147,7 +158,7 @@ export const SignupForm = () => {
variant="darkCTA"
className="w-full justify-center"
loading={signingUp}
disabled={!isButtonEnabled}>
disabled={formRef.current ? (!isButtonEnabled || !isValid): !isButtonEnabled}>
Continue with Email
</Button>
</form>

View File

@@ -0,0 +1,42 @@
"use client";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
export interface PasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {}
const PasswordInput = ({ className, ...rest }: PasswordInputProps) => {
const [showPassword, setShowPassword] = useState(false);
const togglePasswordVisibility = () => {
setShowPassword((prevShowPassword) => !prevShowPassword);
};
return (
<div className="relative">
<input
type={showPassword ? "text" : "password"}
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...rest}
/>
<button
type="button"
className={cn("absolute top-1/2 right-3 transform -translate-y-1/2")}
onClick={togglePasswordVisibility}
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-slate-400 " />
) : (
<EyeIcon className="h-5 w-5 text-slate-400 " />
)}
</button>
</div>
);
};
export { PasswordInput };

View File

@@ -27,6 +27,7 @@ export {
export { Editor, AddVariablesDropdown } from "./components/editor";
export { ErrorComponent } from "./components/ErrorComponent";
export { Input } from "./components/Input";
export { PasswordInput } from "./components/PasswordInput";
export { Label } from "./components/Label";
export { PageTitle } from "./components/PageTitle";
export { Popover, PopoverTrigger, PopoverContent } from "./components/Popover";