Skip to content

Commit

Permalink
feat: add async email validation WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
markkovari committed Oct 11, 2024
1 parent b99fbce commit 3c29c3d
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 33 deletions.
13 changes: 13 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"axios": "^1.7.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"debounce": "^2.1.1",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.451.0",
"next-themes": "^0.3.0",
Expand Down
34 changes: 33 additions & 1 deletion frontend/src/components/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { Card, CardContent, CardFooter, CardHeader } from "./ui/card";
import { AxiosError } from "axios";
import type { ActiveTab } from "./AuthenticationTabs";
import type React from "react";
import { debouncedValidateEmail } from "@/lib/utils/debouncedValidate";
import { useState } from "react";
import { ReloadIcon } from "@radix-ui/react-icons";

const FormSchema = z
.object({
Expand All @@ -44,6 +47,8 @@ type RegisterFormProps = {

const RegisterForm = ({ setActiveTab }: RegisterFormProps) => {
const { toast } = useToast();
const [isEmailValidationLoading, setIsEmailValidationLoading] =
useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
Expand Down Expand Up @@ -114,7 +119,34 @@ const RegisterForm = ({ setActiveTab }: RegisterFormProps) => {
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="your email" {...field} />
<div className="flex flex-row">
<Input
placeholder="your email"
{...field}
onChange={async (e) => {
setIsEmailValidationLoading(true);
field.onChange(e);
const result = await debouncedValidateEmail({
email: e.target.value,
});
if (result.valid) {
form.setError("email", {
type: "manual",
message: "",
});
} else {
form.setError("email", {
type: "manual",
message: "Email is already in use.",
});
}
setIsEmailValidationLoading(false);
}}
/>
{isEmailValidationLoading && (
<ReloadIcon className="mr-2 h-10 w-10 animate-spin" />
)}
</div>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,25 @@ const vetList = async (request: string) => {
return response.data;
};

export type EmailValidationRequest = {
email: string;
};

export type EmailValidationResponse = {
valid: boolean;
};

const emailValidation = async (_request: EmailValidationRequest) => {
return { valid: false };
};

export {
login,
register,
logout,
updateProfile,
deleteProfile,
emailValidation,
vetList,
petList,
addPet,
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/lib/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function debounce<
ParamsT,
ReturnT,
F extends (...args: ParamsT[]) => Promise<ReturnT>,
>(func: F, wait: number) {
let timeoutId: NodeJS.Timeout | null = null;
return (...args: Parameters<F>): Promise<ReturnType<F>> => {
return new Promise((resolve, reject) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(async () => {
try {
const result = (await func(...args)) as ReturnType<F>;
resolve(result);
} catch (error) {
reject(error);
}
}, wait);
});
};
}

export { debounce };
18 changes: 18 additions & 0 deletions frontend/src/lib/utils/debouncedValidate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { emailValidation } from "@/httpClient";
import { debounce } from "./debounce";
import type {
EmailValidationRequest,
EmailValidationResponse,
} from "../../httpClient";

const DEBOUNCE_TIME_MS = 2000;

const debouncedValidateEmail = debounce<
EmailValidationRequest,
EmailValidationResponse,
typeof emailValidation
>(async (request: EmailValidationRequest) => {
return await emailValidation(request);
}, DEBOUNCE_TIME_MS);

export { debouncedValidateEmail };
64 changes: 32 additions & 32 deletions frontend/src/lib/utils/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
import { z, ZodIssueCode } from "zod";

const nameSchema = z
.string()
.min(2, { message: "Name must be at least 2 characters long." })
.max(40, { message: "Name must be at most 40 characters long." });
.string()
.min(2, { message: "Name must be at least 2 characters long." })
.max(40, { message: "Name must be at most 40 characters long." });

const passwordSchema = z.string().superRefine((val, ctx): val is string => {
const issues: string[] = [];
if (val.length < 4) {
issues.push("at least 4 characters");
}
if (val.length > 40) {
issues.push("less then 40 characters");
}
if (/\s/.test(val)) {
issues.push("no whitespace");
}
if (!/[A-Z]/.test(val)) {
issues.push("at least one uppercase letter");
}
if (!/\d/.test(val)) {
issues.push("at least one digit");
}
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(val)) {
issues.push("at least one special character");
}
if (!/[a-z]/.test(val)) {
issues.push("at least one lowercase letter");
}
if (issues.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Password must contain ${issues.join(", ")}.`,
});
}
return false;
const issues: string[] = [];
if (val.length < 4) {
issues.push("at least 4 characters");
}
if (val.length > 40) {
issues.push("less then 40 characters");
}
if (/\s/.test(val)) {
issues.push("no whitespace");
}
if (!/[A-Z]/.test(val)) {
issues.push("at least one uppercase letter");
}
if (!/\d/.test(val)) {
issues.push("at least one digit");
}
if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(val)) {
issues.push("at least one special character");
}
if (!/[a-z]/.test(val)) {
issues.push("at least one lowercase letter");
}
if (issues.length) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Password must contain ${issues.join(", ")}.`,
});
}
return false;
});

export { passwordSchema, nameSchema };

0 comments on commit 3c29c3d

Please sign in to comment.