-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #646 from hngprojects/Vxrcel-clean
- Loading branch information
Showing
53 changed files
with
1,794 additions
and
244 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion
2
src/app/(admin)/_components/unread-notification-card/UnreadNotificationCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"use client"; | ||
|
||
import CustomButton from "~/components/common/common-button/common-button"; | ||
import { InputOtp } from "~/components/common/Input-otp"; | ||
|
||
export default function VerifyCodePage() { | ||
return ( | ||
<> | ||
<div className="m-auto mb-[120px] mt-[73px] flex flex-col items-center justify-center gap-5 px-6 text-center md:gap-6 lg:mb-[133px] lg:mt-20 lg:w-[551px]"> | ||
<div className="flex flex-col items-center gap-2 md:gap-4"> | ||
<h3 className="text-2xl font-semibold md:text-[28px]"> | ||
Verification Code | ||
</h3> | ||
<p className="text-[13px] font-normal text-[#525252] md:text-lg"> | ||
Confirm the OTP sent to{" "} | ||
<span className="font-semibold">[email protected]</span> and | ||
enter the Verification code that was sent. Code expires in{" "} | ||
<span className="font-bold text-[#F97316]">00:59</span> | ||
</p> | ||
</div> | ||
|
||
<div> | ||
<InputOtp /> | ||
</div> | ||
<div className="flex w-full flex-col gap-2 md:gap-4"> | ||
<CustomButton variant="subtle" size="lg" isDisabled={true}> | ||
Verify{" "} | ||
</CustomButton> | ||
<p className="text-[13px]"> | ||
Didn't recieve any code?{" "} | ||
<span className="ml-2 text-base font-semibold text-[#F97316]"> | ||
Resend OTP | ||
</span> | ||
</p> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} |
211 changes: 211 additions & 0 deletions
211
src/app/(auth-routes)/sign-up/_component/SignupForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
"use client"; | ||
|
||
import { Eye, EyeOff } from "lucide-react"; | ||
import { useState } from "react"; | ||
import { z, ZodType } from "zod"; | ||
|
||
import CustomButton from "~/components/common/common-button/common-button"; | ||
|
||
interface IData { | ||
fullName: string; | ||
email: string; | ||
password: string; | ||
} | ||
|
||
interface TestFormProperties { | ||
onSubmit: (data: IData) => void; | ||
} | ||
|
||
const schemaRegister = z.object({ | ||
fullName: z.string().regex(/^['A-Za-z-]+(?: ['A-Za-z-]+)*$/, { | ||
message: "Enter your full name", | ||
}), | ||
email: z.string().email({ | ||
message: "Enter a valid email format", | ||
}), | ||
password: z | ||
.string() | ||
.min(8, { | ||
message: "Password must be at least 8 characters long.", | ||
}) | ||
.max(20, { | ||
message: "Password should not be more than 20 characters long.", | ||
}), | ||
}); | ||
|
||
type SchemaRegisterType = z.infer<typeof schemaRegister>; | ||
|
||
type ErrorsType = Partial<Record<keyof SchemaRegisterType, string>>; | ||
|
||
const SignupForm = ({ onSubmit }: TestFormProperties) => { | ||
const [formData, setFormData] = useState<SchemaRegisterType>({ | ||
fullName: "", | ||
email: "", | ||
password: "", | ||
}); | ||
const [errors, setErrors] = useState<ErrorsType>({}); | ||
const [passwordVisible, setPasswordVisible] = useState<boolean>(false); | ||
|
||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const { name, value } = event.target; | ||
|
||
if (name in schemaRegister.shape) { | ||
setFormData({ ...formData, [name]: value }); | ||
|
||
const fieldSchema = schemaRegister.shape[ | ||
name as keyof typeof schemaRegister.shape | ||
] as ZodType<string>; | ||
const result = fieldSchema.safeParse(value); | ||
|
||
if (result.success) { | ||
setErrors((previousErrors) => { | ||
const remainingErrors = { ...previousErrors }; | ||
delete remainingErrors[name as keyof SchemaRegisterType]; | ||
return remainingErrors; | ||
}); | ||
} else { | ||
setErrors((previousErrors) => ({ | ||
...previousErrors, | ||
[name]: result.error.errors[0].message, | ||
})); | ||
} | ||
} | ||
}; | ||
|
||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { | ||
event.preventDefault(); | ||
const result = schemaRegister.safeParse(formData); | ||
|
||
if (result.success) { | ||
setErrors({}); | ||
onSubmit(formData); | ||
} else { | ||
const newErrors: ErrorsType = {}; | ||
for (const error of result.error.errors) { | ||
const fieldName = error.path[0] as keyof SchemaRegisterType; | ||
newErrors[fieldName] = error.message; | ||
} | ||
setErrors(newErrors); | ||
} | ||
}; | ||
return ( | ||
<form | ||
className="mt-5 flex flex-col space-y-5 text-left lg:space-y-6" | ||
onSubmit={handleSubmit} | ||
> | ||
<div className="flex flex-col"> | ||
<div className="flex flex-col space-y-2 lg:space-y-3"> | ||
<label | ||
htmlFor="fullName" | ||
className="text-xl font-medium lg:font-normal" | ||
> | ||
Full Name | ||
</label> | ||
<input | ||
type="text" | ||
id="fullName" | ||
name="fullName" | ||
value={formData.fullName} | ||
onChange={handleChange} | ||
className={`text-neutralColor-dark-2 rounded-lg border px-4 py-3 text-lg outline-none placeholder:text-[hsl(var(--neutralColor-dark-1))] focus:border-primary lg:py-5 lg:pl-4 lg:pr-6 ${ | ||
errors.fullName | ||
? "border-[#E80D0D]" | ||
: formData.fullName | ||
? "border-primary" | ||
: "border-stroke-colors-stroke" | ||
}`} | ||
placeholder="Enter your full name" | ||
/> | ||
</div> | ||
{errors.fullName && ( | ||
<span className="text-[13px] leading-[180%] text-[#E80D0D]"> | ||
{errors.fullName} | ||
</span> | ||
)} | ||
</div> | ||
<div className="flex flex-col"> | ||
<div className="flex flex-col space-y-2 lg:space-y-3"> | ||
<label | ||
htmlFor="emailAddress" | ||
className="text-xl font-medium lg:font-normal" | ||
> | ||
Email Address | ||
</label> | ||
<input | ||
type="email" | ||
id="emailAddress" | ||
name="email" | ||
value={formData.email} | ||
onChange={handleChange} | ||
className={`text-neutralColor-dark-2 rounded-lg border px-4 py-3 text-lg outline-none placeholder:text-[hsl(var(--neutralColor-dark-1))] focus:border-primary lg:py-5 lg:pl-4 lg:pr-6 ${ | ||
errors.email | ||
? "border-[#E80D0D]" | ||
: formData.email | ||
? "border-primary" | ||
: "border-stroke-colors-stroke" | ||
}`} | ||
placeholder="Enter your email address" | ||
/> | ||
</div> | ||
{errors.email && ( | ||
<span className="text-[13px] leading-[180%] text-[#E80D0D]"> | ||
{errors.email} | ||
</span> | ||
)} | ||
</div> | ||
<div className="flex flex-col"> | ||
<div className="relative flex flex-col space-y-2 lg:space-y-3"> | ||
<label | ||
htmlFor="password" | ||
className="text-xl font-medium lg:font-normal" | ||
> | ||
Password | ||
</label> | ||
<input | ||
type={passwordVisible ? "text" : "password"} | ||
id="password" | ||
name="password" | ||
value={formData.password} | ||
onChange={handleChange} | ||
className={`text-neutralColor-dark-2 rounded-lg border px-4 py-3 text-lg outline-none placeholder:text-[hsl(var(--neutralColor-dark-1))] focus:border-primary lg:py-5 lg:pl-4 lg:pr-6 ${ | ||
errors.password | ||
? "border-[#E80D0D]" | ||
: formData.password | ||
? "border-primary" | ||
: "border-stroke-colors-stroke" | ||
}`} | ||
placeholder="Create your password" | ||
/> | ||
{passwordVisible ? ( | ||
<Eye | ||
size={24} | ||
color="#939393" | ||
className="absolute right-6 top-[50%] cursor-pointer" | ||
onClick={() => setPasswordVisible(!passwordVisible)} | ||
/> | ||
) : ( | ||
<EyeOff | ||
size={24} | ||
color="#939393" | ||
className="absolute right-6 top-[50%] cursor-pointer" | ||
onClick={() => setPasswordVisible(!passwordVisible)} | ||
/> | ||
)} | ||
</div> | ||
{errors.password && ( | ||
<span className="text-[13px] leading-[180%] text-[#E80D0D]"> | ||
{errors.password} | ||
</span> | ||
)} | ||
</div> | ||
<CustomButton | ||
variant="primary" | ||
className="px-4 py-2 text-base font-bold lg:h-16" | ||
> | ||
Create Account | ||
</CustomButton> | ||
</form> | ||
); | ||
}; | ||
|
||
export default SignupForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import userEvent from "@testing-library/user-event"; | ||
|
||
import SignupForm from "./_component/SignupForm"; | ||
import SignUp from "./page"; | ||
|
||
describe("form", () => { | ||
const handleSubmit = vi.fn(); | ||
|
||
it("should render the form fields", () => { | ||
expect.hasAssertions(); | ||
render(<SignUp />); | ||
|
||
expect(screen.getByLabelText("Full Name")).toBeInTheDocument(); | ||
expect(screen.getByLabelText("Email Address")).toBeInTheDocument(); | ||
expect(screen.getByLabelText("Password")).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole("button", { name: "Create Account" }), | ||
).toBeInTheDocument(); | ||
|
||
expect(screen.getByLabelText("Full Name")).toHaveAttribute("type", "text"); | ||
expect(screen.getByLabelText("Email Address")).toHaveAttribute( | ||
"type", | ||
"email", | ||
); | ||
expect(screen.getByLabelText("Password")).toHaveAttribute( | ||
"type", | ||
"password", | ||
); | ||
}); | ||
|
||
it("should not submit the form if required fields are empty", async () => { | ||
expect.hasAssertions(); | ||
render(<SignupForm onSubmit={handleSubmit} />); | ||
|
||
await userEvent.click( | ||
screen.getByRole("button", { name: "Create Account" }), | ||
); | ||
expect(handleSubmit).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("should submit the form with correct values", async () => { | ||
expect.hasAssertions(); | ||
render(<SignupForm onSubmit={handleSubmit} />); | ||
|
||
await userEvent.type(screen.getByLabelText("Full Name"), "John Doe"); | ||
await userEvent.type( | ||
screen.getByLabelText("Email Address"), | ||
"[email protected]", | ||
); | ||
await userEvent.type(screen.getByLabelText("Password"), "securepassword"); | ||
await userEvent.click( | ||
screen.getByRole("button", { name: "Create Account" }), | ||
); | ||
|
||
expect(handleSubmit).toHaveBeenCalledTimes(1); | ||
expect(handleSubmit).toHaveBeenCalledWith({ | ||
fullName: "John Doe", | ||
email: "[email protected]", | ||
password: "securepassword", | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.