feature - added password change, email change, and email confirm forms

This commit is contained in:
James Wyndham 2024-06-05 14:55:51 +08:00
parent 0e2b6e79e5
commit 66ccc4ee4f
10 changed files with 305 additions and 25 deletions

View File

@ -0,0 +1,89 @@
"use client"
import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { changeEmailValidationSchema } from "@/utils/form";
import { toast } from "react-toastify";
import PocketBase from 'pocketbase';
import PageWrapper from "@/components/Utilities/PageWrapper";
import { usePathname, useRouter } from 'next/navigation';
import Background from "@/components/Utilities/Background";
import PageHeader from "@/sections/PageHeader";
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL as string);
export default function ConfirmEmailChangePage() {
const pathName = usePathname();
const token = pathName.split('/').at(-1);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm({
resolver: yupResolver(changeEmailValidationSchema),
});
const onSubmit = async (data: any) => {
try {
await pb.collection('user').confirmEmailChange(
token ?? "",
data.password,
);
reset();
document.getElementById("sign-in-modal")?.click();
} catch (error) {
if (error instanceof Error) {
toast.error("There was a problem. Please try change your email again", {
position: "bottom-left",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
}
}
};
return (
<PageWrapper>
<Background>
<div className="h-screen w-screen flex items-center flex-col">
<PageHeader
title={"Enter Your Password To Change Your Email"}
subtitle={<></>}
/>
<form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-xl px-4">
<div className="relative mt-6">
<input
type="password"
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
placeholder="Password…"
aria-label="Password…"
autoComplete="on"
{...register("password")}
/>
<div className="text-start text-sm italic text-error-content">
{errors.password?.message}&nbsp;
</div>
</div>
<div className="flex flex-row w-full justify-between">
<button
disabled={isSubmitting}
type="submit"
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
>
Change Email
{isSubmitting && <div className="loading"></div>}
</button>
</div>
</form>
</div>
</Background>
</PageWrapper>
);
}

View File

@ -0,0 +1,103 @@
"use client"
import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { passwordValidationSchema } from "@/utils/form";
import { toast } from "react-toastify";
import PocketBase from 'pocketbase';
import PageWrapper from "@/components/Utilities/PageWrapper";
import { usePathname, useRouter } from 'next/navigation';
import Background from "@/components/Utilities/Background";
import PageHeader from "@/sections/PageHeader";
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL as string);
export default function ConfirmPasswordResetPage() {
const pathName = usePathname();
const token = pathName.split('/').at(-1);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm({
resolver: yupResolver(passwordValidationSchema),
});
const onSubmit = async (data: any) => {
try {
await pb.collection('user').confirmPasswordReset(
token ?? "",
data.newPassword,
data.newPasswordConfirm,
);
reset()
document.getElementById("sign-in-modal")?.click();
} catch (error) {
if (error instanceof Error) {
toast.error("There was a problem. Please try reset your password again", {
position: "bottom-left",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
}
}
};
return (
<PageWrapper>
<Background>
<div className="h-screen w-screen flex items-center flex-col">
<PageHeader
title={"Enter Your New Password"}
subtitle={<></>}
/>
<form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-xl px-4">
<div className="relative mt-6">
<input
type="password"
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
placeholder="New password…"
aria-label="New password…"
autoComplete="on"
{...register("newPassword")}
/>
<div className="text-start text-sm italic text-error-content">
{errors.newPassword?.message}&nbsp;
</div>
</div>
<div className="relative mt-6">
<input
type="password"
className="py-3 px-4 block w-full bg-base-200 text-base-content border-primary/40 rounded-lg text-sm focus:border-secondary focus:ring-secondary disabled:opacity-50 disabled:pointer-events-none "
placeholder="Confirm new password…"
aria-label="Confirm new password…"
autoComplete="on"
{...register("newPasswordConfirm")}
/>
<div className="text-start text-sm italic text-error-content">
{errors.newPasswordConfirm?.message}&nbsp;
</div>
</div>
<div className="flex flex-row w-full justify-between">
<button
disabled={isSubmitting}
type="submit"
className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
>
Reset Password
{isSubmitting && <div className="loading"></div>}
</button>
</div>
</form>
</div>
</Background>
</PageWrapper>
);
}

View File

@ -0,0 +1,60 @@
"use client"
import React, { useEffect } from "react";
import { toast } from "react-toastify";
import PocketBase from 'pocketbase';
import PageWrapper from "@/components/Utilities/PageWrapper";
import { usePathname, useRouter } from 'next/navigation';
import Background from "@/components/Utilities/Background";
import PageHeader from "@/sections/PageHeader";
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL as string);
export default function ConfirmVerification() {
const pathName = usePathname();
const router = useRouter();
const token = pathName.split('/').at(-1);
useEffect(() => {
(async () => {
try {
await pb.collection('user').confirmVerification(
token ?? ""
);
} catch (error) {
if (error instanceof Error) {
toast.error("There was a problem. Please try confirm your email again", {
position: "bottom-left",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
});
}
}
})()
}, [])
const handleSignInRedirect = () => {
document.getElementById("sign-in-modal")?.click();
};
return (
<PageWrapper>
<Background>
<div className="h-screen w-screen flex items-center flex-col">
<PageHeader
title={"Your Email Was Verified"}
subtitle={<></>}
/>
<button onClick={handleSignInRedirect} className="btn btn-primary mt-4">
Go to Sign In
</button>
</div>
</Background>
</PageWrapper>
);
}

View File

@ -46,7 +46,7 @@ export default async function RootLayout({
<GTagProvider /> <GTagProvider />
<body className={`${arimo.className} bg-base-100 flex`}> <body className={`${arimo.className} bg-base-100 flex`}>
<ThemeProvider> <ThemeProvider>
<Header isUserLoggedIn={isUserLoggedIn} /> <Header isUserLoggedIn={isUserLoggedIn} authString={cookies().get("pb_auth")?.value || ""} />
{children} {children}
<ToastContainer <ToastContainer
position="bottom-left" position="bottom-left"

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import Navigation from "@/components/Navigation"; import Navigation from "@/components/Navigation";
import ModalSignUp from "@/components/Modals/ModalSignUp"; import ModalSignUp from "@/components/Modals/ModalSignUp";
import ModalSignIn from "@/components/Modals/ModalSignIn"; import ModalSignIn from "@/components/Modals/ModalSignIn";
@ -12,22 +12,27 @@ import { useTheme } from "next-themes";
import Link from "next/link"; import Link from "next/link";
import ModalPasswordReset from "./Modals/ModalPasswordReset"; import ModalPasswordReset from "./Modals/ModalPasswordReset";
import {Person24Filled, SignOut24Filled} from '@fluentui/react-icons' import {Person24Filled, SignOut24Filled} from '@fluentui/react-icons'
import ModalEmailChange from "./Modals/ModalEmailChange";
import { usePathname } from "next/navigation";
interface HeaderProps { interface HeaderProps {
isUserLoggedIn: boolean; isUserLoggedIn: boolean;
authString: string;
} }
export default function Header({ isUserLoggedIn }: HeaderProps) { export default function Header({ isUserLoggedIn, authString }: HeaderProps) {
//Advice: Remove this and replace it with proper state management like Redux, Recoil or Zustand //Advice: Remove this and replace it with proper state management like Redux, Recoil or Zustand
const [user, setUser] = useState<User | undefined>(); const [user, setUser] = useState<User | undefined>();
const [statefulUser, setStatefulUser] = useState<User | undefined>(); const [statefulUser, setStatefulUser] = useState<User | undefined>();
const pathName = usePathname();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const user = await getUser(); const user = await getUser();
setUser(user as User); setUser(user as User);
})(); })();
}, [statefulUser]); }, [statefulUser]);
//End Advice
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
return ( return (
<header className="absolute w-full z-30"> <header className="absolute w-full z-30">
@ -150,6 +155,7 @@ export default function Header({ isUserLoggedIn }: HeaderProps) {
</div> </div>
<ModalSignIn /> <ModalSignIn />
<ModalPasswordReset /> <ModalPasswordReset />
<ModalEmailChange authString={authString} />
<ModalSignUp setUser={setStatefulUser} /> <ModalSignUp setUser={setStatefulUser} />
</header> </header>
); );

View File

@ -1,28 +1,30 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { signInValidationSchema } from "@/utils/form"; import { emailValidationSchema } from "@/utils/form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import pb from "@/lib/pocketbase"; import pb from "@/lib/pocketbase";
import {Dismiss20Filled} from "@fluentui/react-icons" import {Dismiss20Filled} from "@fluentui/react-icons"
import { redirect } from "next/navigation";
function ModalEmailChange() { function ModalEmailChange({authString}:{authString:string}) {
const { const {
register, register,
handleSubmit, handleSubmit,
reset, reset,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm({ } = useForm({
resolver: yupResolver(signInValidationSchema), resolver: yupResolver(emailValidationSchema),
}); });
const onSubmit = async (data: any) => { const onSubmit = async (data: any) => {
console.log(data); console.log(data);
pb.authStore.loadFromCookie(authString);
!pb.authStore.isValid && redirect("/");
try { try {
//login user if (await pb.collection("user").requestEmailChange(data.email)) {
if (await pb.collection("user").requestPasswordReset(data.email)) {
reset(); reset();
document.getElementById("password-reset-modal")?.click(); document.getElementById("email-change-modal")?.click();
document.getElementById("sign-in-modal")?.click(); document.getElementById("sign-in-modal")?.click();
} }
} catch (error) { } catch (error) {
@ -44,17 +46,17 @@ function ModalEmailChange() {
<> <>
<input <input
type="checkbox" type="checkbox"
id="password-reset-modal" id="email-change-modal"
className="modal-toggle" className="modal-toggle"
onClick={() => { onClick={() => {
reset(); reset();
}} }}
/> />
<label htmlFor="password-reset-modal" className="modal cursor-pointer"> <label htmlFor="email-change-modal" className="modal cursor-pointer">
<label className="modal-box relative bg-base-100 max-w-full md:max-w-[450px] py-4 px-3 md:p-6"> <label className="modal-box relative bg-base-100 max-w-full md:max-w-[450px] py-4 px-3 md:p-6">
<div className="flex justify-end pb-2 select-none"> <div className="flex justify-end pb-2 select-none">
<label <label
htmlFor="password-reset-modal" htmlFor="email-change-modal"
className="cursor-pointer text-base-content" className="cursor-pointer text-base-content"
> >
<Dismiss20Filled /> <Dismiss20Filled />
@ -63,9 +65,9 @@ function ModalEmailChange() {
<div> <div>
<div className="w-[100%] bg-gradient-to-r from-primary to-secondary px-6 pt-2 mt-3 pb-6 rounded-lg text-primary-content"> <div className="w-[100%] bg-gradient-to-r from-primary to-secondary px-6 pt-2 mt-3 pb-6 rounded-lg text-primary-content">
<h3 className="pb-1 text-3xl font-bold md:text-3xl pt-6"> <h3 className="pb-1 text-3xl font-bold md:text-3xl pt-6">
Reset Your Password Change Your Email
</h3> </h3>
<p className="text-sm md:text-base">Enter in your email</p> <p className="text-sm md:text-base">Enter in your new email</p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className="w-full"> <form onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="relative mt-6"> <div className="relative mt-6">
@ -86,9 +88,9 @@ function ModalEmailChange() {
<button <button
disabled={isSubmitting} disabled={isSubmitting}
type="submit" type="submit"
className="btn btn-primary" className={isSubmitting ? "btn btn-gray": "btn btn-primary"}
> >
Reset Password Change Email
{isSubmitting && <div className="loading"></div>} {isSubmitting && <div className="loading"></div>}
</button> </button>
</div> </div>

View File

@ -1,10 +1,11 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { signInValidationSchema } from "@/utils/form"; import { emailValidationSchema } from "@/utils/form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import pb from "@/lib/pocketbase"; import pb from "@/lib/pocketbase";
import {Dismiss20Filled} from '@fluentui/react-icons' import {Dismiss20Filled} from '@fluentui/react-icons'
import { redirect } from "next/navigation";
function ModalPasswordReset() { function ModalPasswordReset() {
const { const {
@ -13,7 +14,7 @@ function ModalPasswordReset() {
reset, reset,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm({ } = useForm({
resolver: yupResolver(signInValidationSchema), resolver: yupResolver(emailValidationSchema),
}); });
const onSubmit = async (data: any) => { const onSubmit = async (data: any) => {

View File

@ -173,7 +173,7 @@ function AccountContent({ user }: ManageSubscriptionProps) {
</button> </button>
<button <button
onClick={() => onClick={() =>
document.getElementById("reset-password-modal")?.click() document.getElementById("password-reset-modal")?.click()
} }
className="btn btn-sm btn-secondary md:ml-auto text-primary-content capitalize border-none" className="btn btn-sm btn-secondary md:ml-auto text-primary-content capitalize border-none"
> >
@ -181,7 +181,7 @@ function AccountContent({ user }: ManageSubscriptionProps) {
</button> </button>
<button <button
onClick={() => onClick={() =>
document.getElementById("change-email-modal")?.click() document.getElementById("email-change-modal")?.click()
} }
className="btn btn-sm btn-secondary text-primary-content capitalize border-none" className="btn btn-sm btn-secondary text-primary-content capitalize border-none"
> >

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import pb from "@/lib/pocketbase"; import pb from "@/lib/pocketbase";
import { newsletterValidationSchema } from "@/utils/form"; import { emailValidationSchema } from "@/utils/form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import React from "react"; import React from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -13,7 +13,7 @@ function Newsletter() {
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
resolver: yupResolver(newsletterValidationSchema), resolver: yupResolver(emailValidationSchema),
}); });
const onSubmit = async (data: { email: string }) => { const onSubmit = async (data: { email: string }) => {

View File

@ -51,6 +51,23 @@ const signUpValidationSchema = Yup.object().shape({
.required("Password is required.") .required("Password is required.")
.min(8, "Password is too short - should be 8 characters minimum.") .min(8, "Password is too short - should be 8 characters minimum.")
}); });
const passwordValidationSchema = Yup.object().shape({
newPassword: Yup.string()
.password()
.required("Password is required.")
.min(8, "Password is too short - should be 8 characters minimum."),
newPasswordConfirm: Yup.string()
.password()
.required("Password is required.")
.min(8, "Password is too short - should be 8 characters minimum.")
});
const changeEmailValidationSchema = Yup.object().shape({
password: Yup.string()
.password()
.required("Password is required.")
.min(8, "Password is too short - should be 8 characters minimum."),
});
const waitinglistValidationSchema = Yup.object().shape({ const waitinglistValidationSchema = Yup.object().shape({
firstName: Yup.string().required("First Name is required"), firstName: Yup.string().required("First Name is required"),
lastName: Yup.string().required("Last Name is required"), lastName: Yup.string().required("Last Name is required"),
@ -66,7 +83,7 @@ const contactUsValidationSchema = Yup.object().shape({
email: Yup.string().email().required("E-mail is required"), email: Yup.string().email().required("E-mail is required"),
}); });
const newsletterValidationSchema = Yup.object().shape({ const emailValidationSchema = Yup.object().shape({
email: Yup.string().email().required("E-mail is required"), email: Yup.string().email().required("E-mail is required"),
}); });
@ -80,7 +97,9 @@ const signInValidationSchema = Yup.object().shape({
}); });
export { export {
newsletterValidationSchema, emailValidationSchema,
changeEmailValidationSchema,
passwordValidationSchema,
signUpValidationSchema, signUpValidationSchema,
signInValidationSchema, signInValidationSchema,
waitinglistValidationSchema, waitinglistValidationSchema,