Skip to content

Commit

Permalink
feat: Implement change password form (#73)
Browse files Browse the repository at this point in the history
Co-authored-by: Rudra Patel <patelrudra2003@gmail.com>
  • Loading branch information
Trishu-Patel and RudraPatel2003 authored Dec 16, 2024
1 parent f62bc1a commit 69a6b3a
Show file tree
Hide file tree
Showing 14 changed files with 338 additions and 59 deletions.
30 changes: 30 additions & 0 deletions src/app/change-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Box } from "@mui/material";
import { redirect } from "next/navigation";

import ChangePasswordForm from "@/components/ChangePassword/ChangePasswordForm";
import getUserSession from "@/utils/getUserSession";

export default async function ForgotPasswordPage() {
const session = await getUserSession();

if (!session) {
redirect("/login");
}

return (
<Box
sx={{
height: "100vh",
width: "100vw",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<ChangePasswordForm
firstName={session.user.firstName}
email={session.user.email}
/>
</Box>
);
}
158 changes: 158 additions & 0 deletions src/components/ChangePassword/ChangePasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import LoadingButton from "@mui/lab/LoadingButton";
import { Box, Snackbar, Typography } from "@mui/material";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import ControlledTextField from "@/components/controlled/ControlledTextField";
import { handleChangePassword } from "@/server/api/users/public-mutations";

const changePasswordFormSchema = z
.object({
oldPassword: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
newPassword: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
confirmPassword: z.string().min(8, {
message: "Password must be at least 8 characters",
}),
})
.superRefine((val, ctx) => {
if (val.newPassword !== val.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Passwords do not match",
path: ["confirmPassword"],
});
}
});

type ChangePasswordFormValues = z.infer<typeof changePasswordFormSchema>;

type ChangePasswordFormProps = {
firstName: string;
email: string;
};

export default function ChangePasswordForm({
firstName,
email,
}: ChangePasswordFormProps) {
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [disabled, setDisabled] = useState(false);
const router = useRouter();

const {
control,
handleSubmit,
formState: { errors },
} = useForm<ChangePasswordFormValues>({
resolver: zodResolver(changePasswordFormSchema),
defaultValues: {
oldPassword: "",
newPassword: "",
confirmPassword: "",
},
});

const onSubmit = async (data: ChangePasswordFormValues) => {
setIsLoading(true);

const { oldPassword, newPassword } = data;
const [, error] = await handleChangePassword(
firstName,
email,
oldPassword,
newPassword,
);

if (error === null) {
setSnackbarMessage("Password successfully changed");
setSnackbarOpen(true);

setTimeout(() => {
router.push("/settings");
}, 1000);
} else {
setSnackbarMessage("Password change failed.");
setSnackbarOpen(true);

setIsLoading(false);
setDisabled(true);
}

setIsLoading(false);
setDisabled(true);
};

return (
<>
<Snackbar
open={snackbarOpen}
autoHideDuration={6000}
onClose={() => setSnackbarOpen(false)}
message={snackbarMessage}
/>
<form onSubmit={handleSubmit(onSubmit)}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "grid",
gap: 1.5,
gridTemplateColumns: "1fr",
boxShadow: 2,
borderRadius: 2,
padding: 3,
}}
>
<Typography variant="h4">Change Password?</Typography>

<ControlledTextField
control={control}
name="oldPassword"
label="Old Password"
variant="outlined"
error={errors.oldPassword}
type="password"
/>

<ControlledTextField
control={control}
name="newPassword"
label="New Password"
variant="outlined"
error={errors.newPassword}
type="password"
/>

<ControlledTextField
control={control}
name="confirmPassword"
label="Confirm New Password"
variant="outlined"
error={errors.confirmPassword}
type="password"
/>

<LoadingButton
type="submit"
variant="contained"
color="primary"
loading={isLoading}
disabled={disabled}
>
Change Password
</LoadingButton>
</Box>
</form>
</>
);
}
10 changes: 9 additions & 1 deletion src/components/EnrollmentForm/DisqualifiedFormSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import Link from "next/link";

export default function DisqualifiedFormSection() {
return (
<Box sx={{ width: "min(90vw, 700px)", textAlign: "center" }}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<Typography variant="body1">
We are sorry, but you do not qualify for any of the programs we offer.
Please contact us at our{" "}
Expand Down
10 changes: 9 additions & 1 deletion src/components/EnrollmentForm/SubmittedFormSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ export default function SubmittedFormSection() {
}, []);

return (
<Box sx={{ width: "min(90vw, 700px)", textAlign: "center" }}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<Typography variant="body1">
Thank you for completing the enrollment form!
</Typography>
Expand Down
10 changes: 9 additions & 1 deletion src/components/ForgotPassword/InvalidPasswordResetToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import Link from "next/link";

export default function InvalidPasswordResetToken() {
return (
<Box sx={{ width: "min(90vw, 700px)", textAlign: "center" }}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<Typography variant="body1">This link has expired</Typography>
<Link href="/forgot-password" style={{ textDecoration: "none" }}>
<Typography variant="body1" color="primary">
Expand Down
10 changes: 9 additions & 1 deletion src/components/NotFound/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import Link from "next/link";

export default function NotFound() {
return (
<Box sx={{ width: "min(90vw, 700px)", textAlign: "center" }}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<Typography variant="body1">
The page you are looking for does not exist.
</Typography>
Expand Down
10 changes: 3 additions & 7 deletions src/components/Settings/AdminSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, Typography } from "@mui/material";
import Link from "next/link";
import { Box, Typography } from "@mui/material";

import ChangePasswordButton from "@/components/Settings/ChangePasswordButton";
import SignOutButton from "@/components/Settings/SignOutButton";
import { User } from "@/types";

Expand All @@ -23,11 +23,7 @@ export default function AdminSettings({ user }: AdminSettingsProps) {
Name: {user.firstName} {user.lastName}
</Typography>
<Typography variant="body1">Email: {user.email}</Typography>
<Link href="/change-password" passHref>
<Button variant="contained" color="primary">
Reset Password
</Button>
</Link>
<ChangePasswordButton />
<SignOutButton />
</Box>
);
Expand Down
12 changes: 12 additions & 0 deletions src/components/Settings/ChangePasswordButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Button } from "@mui/material";
import Link from "next/link";

export default function ChangePasswordButton() {
return (
<Link href="/change-password" passHref>
<Button variant="contained" color="primary">
Change Password
</Button>
</Link>
);
}
10 changes: 3 additions & 7 deletions src/components/Settings/ClientSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, Typography } from "@mui/material";
import Link from "next/link";
import { Box, Typography } from "@mui/material";

import ChangePasswordButton from "@/components/Settings/ChangePasswordButton";
import SignOutButton from "@/components/Settings/SignOutButton";
import { User } from "@/types";

Expand All @@ -23,11 +23,7 @@ export default function ClientSettings({ user }: ClientSettingsProps) {
Name: {user.firstName} {user.lastName}
</Typography>
<Typography variant="body1">Email: {user.email}</Typography>
<Link href="/change-password" passHref>
<Button variant="contained" color="primary">
Reset Password
</Button>
</Link>
<ChangePasswordButton />
<SignOutButton />
</Box>
);
Expand Down
10 changes: 9 additions & 1 deletion src/components/VerifyEmail/InvalidEmailVerificationToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import Link from "next/link";

export default function InvalidEmailVerificationToken() {
return (
<Box sx={{ width: "min(90vw, 700px)", textAlign: "center" }}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<Typography variant="body1">This link has expired</Typography>
<Link href="/verify-email" style={{ textDecoration: "none" }}>
<Typography variant="body1" color="primary">
Expand Down
10 changes: 9 additions & 1 deletion src/components/VerifyEmail/VerifyEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ export default function VerifyEmail({ email }: VerifyEmailProps) {
onClose={() => setSnackbarOpen(false)}
message="Email verification email sent"
/>
<Box sx={{ width: "min(90vw, 700px)", textAlign: "center" }}>
<Box
sx={{
width: "min(90vw, 700px)",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<Typography variant="body1">
Please verify your email address by clicking on the link in the email
we sent you.
Expand Down
24 changes: 19 additions & 5 deletions src/components/VerifyEmail/VerifyEmailSuccess.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";

import { Typography } from "@mui/material";
import { Snackbar, Typography } from "@mui/material";
import { useRouter } from "next/navigation";
import { signOut } from "next-auth/react";
import { useEffect } from "react";
import { useEffect, useState } from "react";

import { verifyEmailWithToken } from "@/server/api/users/public-mutations";
import { EmailVerificationToken } from "@/types";
Expand All @@ -16,17 +16,31 @@ type VerifyEmailSuccessProps = {
export default function VerifyEmailSuccess({
emailVerificationToken,
}: VerifyEmailSuccessProps) {
const [snackbarOpen, setSnackbarOpen] = useState(false);
const router = useRouter();

useEffect(() => {
const verifyEmail = async () => {
await verifyEmailWithToken(emailVerificationToken.token);
await signOut();
router.push("/");
setSnackbarOpen(true);
setTimeout(async () => {
await signOut();
router.push("/");
}, 1000);
};

void verifyEmail();
}, []);

return <Typography variant="body1">Verifying email...</Typography>;
return (
<>
<Snackbar
open={snackbarOpen}
autoHideDuration={3000}
onClose={() => setSnackbarOpen(false)}
message="Email verified"
/>
<Typography variant="body1">Verifying email...</Typography>;
</>
);
}
Loading

0 comments on commit 69a6b3a

Please sign in to comment.