diff --git a/components/modal/auth/InputField.tsx b/components/modal/auth/InputField.tsx index e4e5fbd..2d5c43b 100644 --- a/components/modal/auth/InputField.tsx +++ b/components/modal/auth/InputField.tsx @@ -1,41 +1,15 @@ -import { Input } from "@chakra-ui/react"; - -/** - * @param {string} name - Name of the input field - * @param {string} placeholder - Placeholder text - * @param {string} type - Type of the input field - * @param {React.ChangeEventHandler} onChange - On change event handler - */ -interface InputFieldProps { - name: string; - placeholder: string; - type: string; - onChange: React.ChangeEventHandler; -} +import { Input, InputProps } from "@chakra-ui/react"; +import React, { forwardRef } from "react"; /** * Input field for various forms used in the Auth modal component. - * @param {string} name - Name of the input field - * @param {string} placeholder - Placeholder text - * @param {string} type - Type of the input field - * @param {React.ChangeEventHandler} onChange - On change event handler - * - * @returns {React.FC} - Input field component */ -const InputField: React.FC = ({ - name, - placeholder, - type, - onChange, -}) => { +const InputField = forwardRef((props, ref) => { return ( = ({ }} /> ); -}; +}); + +InputField.displayName = "InputField"; export default InputField; diff --git a/components/modal/auth/Login.tsx b/components/modal/auth/Login.tsx index 0c93424..c60fa5a 100644 --- a/components/modal/auth/Login.tsx +++ b/components/modal/auth/Login.tsx @@ -1,12 +1,15 @@ -import { Button, Flex, Input, Text } from "@chakra-ui/react"; +import { Button, Flex, Text } from "@chakra-ui/react"; import { useSetAtom } from "jotai"; -import React, { useState } from "react"; +import React from "react"; import { useSignInWithEmailAndPassword } from "react-firebase-hooks/auth"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { authModalStateAtom } from "../../../atoms/authModalAtom"; import { auth } from "../../../firebase/clientApp"; import { FIREBASE_ERRORS } from "../../../firebase/errors"; import InputField from "./InputField"; import { PasswordInput } from "@/components/ui/password-input"; +import { loginSchema, LoginInput } from "@/schema/auth"; type LoginProps = {}; @@ -25,66 +28,34 @@ type LoginProps = {}; */ const Login: React.FC = () => { const setAuthModalState = useSetAtom(authModalStateAtom); // Set global state - const [loginForm, setLoginForm] = useState({ - email: "", // Initially empty email - password: "", // Initially empty password - }); - const [signInWithEmailAndPassword, user, loading, error] = useSignInWithEmailAndPassword(auth); - /** - * This function is used as the event handler for a form submission. - * It will prevent the page from refreshing. - * Automatically checks if the user with the email exists and if the password is correct. - * @param {React.FormEvent} event - the submit event triggered by the form - */ - const onSubmit = (event: React.FormEvent) => { - event.preventDefault(); // Prevent page from reloading - - signInWithEmailAndPassword(loginForm.email, loginForm.password); // Sign in with email and password - }; // Function to execute when the form is submitted - - /** - * Function to execute when the form is changed (when email and password are typed). - * Multiple inputs use the same `onChange` function. - * @param event (React.ChangeEvent) - the event that is triggered when the form is changed - */ - const onChange = (event: React.ChangeEvent) => { - // Update form state - setLoginForm((prev) => ({ - ...prev, // Spread previous state because we don't want to lose the other input's value - [event.target.name]: event.target.value, // Catch the name of the input that was changed and update the corresponding state - })); - }; + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(loginSchema), + mode: "onChange", + }); - /** - * Determines whether the button is disabled or not. - * The button is disabled if the email or password is empty. - * @returns {boolean} - Whether the button is disabled or not - */ - const isButtonDisabled = () => { - return ( - !loginForm.email || !loginForm.password - // signUpForm.confirmPassword !== signUpForm.password - ); + const onSubmit = (data: LoginInput) => { + signInWithEmailAndPassword(data.email, data.password); }; return ( -
- + + + {errors.email && ( + + {errors.email.message} + + )} = () => { border: "1px solid", borderColor: { base: "red.500", _dark: "red.400" }, }} + {...register("password")} /> + {errors.password && ( + + {errors.password.message} + + )} {FIREBASE_ERRORS[error?.code as keyof typeof FIREBASE_ERRORS]} @@ -118,7 +96,7 @@ const Login: React.FC = () => { mb={2} type="submit" loading={loading} - disabled={isButtonDisabled()} + disabled={!isValid} > {" "} {/* When the form is submitted, execute onSubmit function */} diff --git a/components/modal/auth/ResetPassword.tsx b/components/modal/auth/ResetPassword.tsx index d786148..e68e7e5 100644 --- a/components/modal/auth/ResetPassword.tsx +++ b/components/modal/auth/ResetPassword.tsx @@ -2,61 +2,52 @@ import { Button, Flex, Icon, Image, Input, Text } from "@chakra-ui/react"; import { useSetAtom } from "jotai"; import React, { useState } from "react"; import { useSendPasswordResetEmail } from "react-firebase-hooks/auth"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { BsDot } from "react-icons/bs"; import { authModalStateAtom } from "../../../atoms/authModalAtom"; import { auth } from "../../../firebase/clientApp"; +import { resetPasswordSchema, ResetPasswordInput } from "@/schema/auth"; -/** - * Allows the user to reset their password. - * Takes the email as the input and sends the user an email from Firebase to reset the password. - * Once the email is submitted, a new view is shown telling the user to check their email. - * @returns {React.FC} - Reset Password view in the authentication modal - * - * @see https://github.com/CSFrequency/react-firebase-hooks/tree/master/auth - */ const ResetPassword: React.FC = () => { const setAuthModalState = useSetAtom(authModalStateAtom); - const [email, setEmail] = useState(""); const [success, setSuccess] = useState(false); const [sendPasswordResetEmail, sending, error] = useSendPasswordResetEmail(auth); - /** - * This function is used as the event handler for a form submission. - * It will prevent the page from refreshing. - * Sends the email from Firebase to the email that was inputted in the form. - * @param {React.FormEvent} event - the submit event triggered by the form - */ - const onSubmit = async (event: React.FormEvent) => { - event.preventDefault(); // Prevent page from reloading + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(resetPasswordSchema), + mode: "onChange", + }); - await sendPasswordResetEmail(email); // try to send email - setSuccess(true); // once the email is successfully send + const onSubmit = async (data: ResetPasswordInput) => { + await sendPasswordResetEmail(data.email); + setSuccess(true); }; + return ( Website logo Reset your password - {/* Go to next page once the email is successfully sent */} {success ? ( Check your email ) : ( - // While the email has not been sent, show the form <> Enter the email associated with your account and we will send you a reset link - + setEmail(event.target.value)} fontSize="10pt" _placeholder={{ color: "gray.500" }} _hover={{ @@ -71,7 +62,13 @@ const ResetPassword: React.FC = () => { borderColor: "blue.500", }} bg={{ base: "gray.50", _dark: "gray.800" }} + {...register("email")} /> + {errors.email && ( + + {errors.email.message} + + )} { mt={2} type="submit" loading={sending} + disabled={!isValid} > Reset Password diff --git a/components/modal/auth/Signup.tsx b/components/modal/auth/Signup.tsx index 28c0cba..9b4b530 100644 --- a/components/modal/auth/Signup.tsx +++ b/components/modal/auth/Signup.tsx @@ -1,98 +1,46 @@ -import { validateSignupForm } from "@/lib/validation"; import { Button, Flex, Text } from "@chakra-ui/react"; import { useSetAtom } from "jotai"; -import React, { useState } from "react"; +import React from "react"; import { useCreateUserWithEmailAndPassword } from "react-firebase-hooks/auth"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { authModalStateAtom } from "../../../atoms/authModalAtom"; import { auth } from "../../../firebase/clientApp"; import { FIREBASE_ERRORS } from "../../../firebase/errors"; import InputField from "./InputField"; import { PasswordInput } from "@/components/ui/password-input"; +import { signUpSchema, SignUpInput } from "@/schema/auth"; -/** - * Allows the user to create an account by inputting the required credentials (email and password). - * There are 2 password fields to ensure that the user inputs the correct password. - * If the 2 passwords do not match, the account is not created and an error is displayed. - * If the email already exists, the account is not created and an error is displayed. - * - * A button to log in instead is available which would switch the modal to the log in view when clicked. - * @returns {React.FC} - Sign up components view for modal. - * - * @see https://github.com/CSFrequency/react-firebase-hooks/tree/master/auth - */ const SignUp = () => { - const setAuthModalState = useSetAtom(authModalStateAtom); // Set global state - const [signUpForm, setSignUpForm] = useState({ - email: "", // Initially empty email - password: "", // Initially empty password - confirmPassword: "", // Initially empty confirm password - }); - const [error, setError] = useState(""); // Initially empty error - const [ - createUserWithEmailAndPassword, // returns a function that returns the user, loading or error - user, - loading, - userError, - ] = useCreateUserWithEmailAndPassword(auth); - - /** - * This function is used as the event handler for a form submission. - * It will prevent the page from refreshing. - * Checks if the password and confirm password fields match and the password requirements are met: - * - If they do not match, an error message is set and the function returns without creating a new user. - * - If the password does not meet the requirements, an error message is set and the function returns without creating a new user. - * - If the passwords match and the password meets the requirements, a new user is created using the email and password provided in the form. - * @param {React.FormEvent} event - the submit event triggered by the form - * - * @returns exit if there is an error or the passwords do not match - */ - const onSubmit = (event: React.FormEvent) => { - event.preventDefault(); // Prevent the page from refreshing - if (error) setError(""); // If there is an error, clear it - - const validationError = validateSignupForm(signUpForm); - if (validationError) { - setError(validationError); - return; - } + const setAuthModalState = useSetAtom(authModalStateAtom); + const [createUserWithEmailAndPassword, user, loading, userError] = + useCreateUserWithEmailAndPassword(auth); - createUserWithEmailAndPassword(signUpForm.email, signUpForm.password); // Create user with email and password - }; // Function to execute when the form is submitted - - /** - * Function to execute when the form is changed (when email and password are typed). - * Multiple inputs use the same onChange function. - * @param {React.ChangeEvent} event - the change event triggered by the input - */ - const onChange = (event: React.ChangeEvent) => { - // Update form state - setSignUpForm((prev) => ({ - ...prev, // Spread previous state because we don't want to lose the other input's value - [event.target.name]: event.target.value, // Catch the name of the input that was changed and update the corresponding state - })); - }; + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(signUpSchema), + mode: "onChange", + }); - const isButtonDisabled = () => { - return ( - !signUpForm.email || !signUpForm.password || !signUpForm.confirmPassword - ); + const onSubmit = (data: SignUpInput) => { + createUserWithEmailAndPassword(data.email, data.password); }; return ( - - + + + {errors.email && ( + + {errors.email.message} + + )} { border: "1px solid", borderColor: { base: "red.500", _dark: "red.400" }, }} + {...register("password")} /> + {errors.password && ( + + {errors.password.message} + + )} { border: "1px solid", borderColor: { base: "red.500", _dark: "red.400" }, }} + {...register("confirmPassword")} /> - - {/* If there is error than the error is shown */} + {errors.confirmPassword && ( + + {errors.confirmPassword.message} + + )} - {error || + {userError && FIREBASE_ERRORS[userError?.code as keyof typeof FIREBASE_ERRORS]} @@ -151,11 +107,9 @@ const SignUp = () => { mt={2} mb={2} type="submit" - loading={loading} // If loading (from Firebase) is true, show loading spinner - disabled={isButtonDisabled()} + loading={loading} + disabled={!isValid} > - {" "} - {/* When the form is submitted, execute onSubmit function */} Sign Up diff --git a/components/modal/community-settings/AdminManager.tsx b/components/modal/community-settings/AdminManager.tsx index d222c73..1eb19ed 100644 --- a/components/modal/community-settings/AdminManager.tsx +++ b/components/modal/community-settings/AdminManager.tsx @@ -18,6 +18,9 @@ import React, { useEffect, useState } from "react"; import { useAuthState } from "react-firebase-hooks/auth"; import { AdminUser } from "@/types/adminUser"; import ConfirmationDialog from "@/components/modal/ConfirmationDialog"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { addAdminSchema, AddAdminInput } from "@/schema/admin"; type AdminManagerProps = { communityData: Community; @@ -34,7 +37,21 @@ const AdminManager: React.FC = ({ communityData }) => { const { searchUsers, findUser } = useAdminSearch(); const { handleAddAdmin } = useAddAdmin(); const { handleRemoveAdmin } = useRemoveAdmin(); - const [newAdminEmail, setNewAdminEmail] = useState(""); + + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(addAdminSchema), + defaultValues: { email: "" }, + }); + + const emailValue = watch("email"); + const [addingAdmin, setAddingAdmin] = useState(false); const [searchResults, setSearchResults] = useState([]); const [showResults, setShowResults] = useState(false); @@ -55,12 +72,11 @@ const AdminManager: React.FC = ({ communityData }) => { ); }, [communityData, loadAdmins, showToast]); - const onAddAdmin = async () => { - if (!newAdminEmail) return; + const onAddAdmin = async (data: AddAdminInput) => { setAddingAdmin(true); try { // 1. Find user by email - const newUser = await findUser(newAdminEmail); + const newUser = await findUser(data.email); if (!newUser) { showToast({ @@ -91,7 +107,7 @@ const AdminManager: React.FC = ({ communityData }) => { setAdmins ); - setNewAdminEmail(""); + reset(); showToast({ title: "Admin added", @@ -137,13 +153,13 @@ const AdminManager: React.FC = ({ communityData }) => { useEffect(() => { const searchUsersAsync = async () => { - if (newAdminEmail.length < 3) { + if (!emailValue || emailValue.length < 3) { setSearchResults([]); setShowResults(false); return; } try { - const results = await searchUsers(newAdminEmail); + const results = await searchUsers(emailValue); // Filter out existing admins const filtered = results.filter( (u) => !admins.some((a) => a.uid === u.uid) @@ -157,7 +173,7 @@ const AdminManager: React.FC = ({ communityData }) => { const timer = setTimeout(searchUsersAsync, 300); return () => clearTimeout(timer); - }, [newAdminEmail, admins, searchUsers]); + }, [emailValue, admins, searchUsers]); return ( @@ -166,22 +182,30 @@ const AdminManager: React.FC = ({ communityData }) => { - - setNewAdminEmail(e.target.value)} - onFocus={() => newAdminEmail.length >= 3 && setShowResults(true)} - onBlur={() => setTimeout(() => setShowResults(false), 200)} - borderRadius={"xl"} - /> - + + + + emailValue && emailValue.length >= 3 && setShowResults(true) + } + onBlur={() => setTimeout(() => setShowResults(false), 200)} + borderRadius={"xl"} + /> + + + {errors.email && ( + + {errors.email.message} + + )} {showResults && searchResults.length > 0 && ( = ({ communityData }) => { cursor="pointer" _hover={{ bg: "gray.100", _dark: { bg: "gray.600" } }} onClick={() => { - setNewAdminEmail(user.email); + setValue("email", user.email); setShowResults(false); }} > diff --git a/components/modal/create-community/CommunityNameSection.tsx b/components/modal/create-community/CommunityNameSection.tsx index d48ac97..48659bf 100644 --- a/components/modal/create-community/CommunityNameSection.tsx +++ b/components/modal/create-community/CommunityNameSection.tsx @@ -1,26 +1,25 @@ import React from "react"; -import { Box, Text, Input } from "@chakra-ui/react"; +import { Box, Text, Input, InputProps } from "@chakra-ui/react"; +import { UseFormRegisterReturn } from "react-hook-form"; -interface CommunityNameSectionProps { - communityName: string; - handleChange: (event: React.ChangeEvent) => void; - charRemaining: number; - error: string; +interface CommunityNameSectionProps extends InputProps { + charRemaining?: number; + error?: string; + register?: UseFormRegisterReturn; } /** * Input block for community name entry with character counter and errors. - * @param communityName - Current name value. - * @param handleChange - Change handler enforced by the modal. * @param charRemaining - Remaining characters allowed. * @param error - Validation error message to display. + * @param register - React Hook Form register object. * @returns Form section for naming a community. */ const CommunityNameSection: React.FC = ({ - communityName, - handleChange, charRemaining, error, + register, + ...rest }) => { return ( @@ -32,9 +31,7 @@ const CommunityNameSection: React.FC = ({ = ({ border: "1px solid", borderColor: { base: "red.500", _dark: "red.400" }, }} + {...register} + {...rest} /> {charRemaining} Characters remaining diff --git a/components/modal/create-community/CreateCommunityModal.tsx b/components/modal/create-community/CreateCommunityModal.tsx index 2caebd3..7909367 100644 --- a/components/modal/create-community/CreateCommunityModal.tsx +++ b/components/modal/create-community/CreateCommunityModal.tsx @@ -2,10 +2,6 @@ import { useCreateCommunity } from "@/hooks/community/useCreateCommunity"; import { Box, Button, - CheckboxControl, - CheckboxIndicator, - CheckboxLabel, - CheckboxRoot, DialogBackdrop, DialogBody, DialogCloseTrigger, @@ -15,19 +11,21 @@ import { DialogPositioner, DialogRoot, DialogTitle, - Flex, - Icon, - Input, Separator, Stack, Text, } from "@chakra-ui/react"; -import React, { FC, useState } from "react"; -import { IconType } from "react-icons"; +import React, { FC } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { BsFillEyeFill, BsFillPersonFill } from "react-icons/bs"; import { HiLockClosed } from "react-icons/hi"; import CommunityTypeOptions from "./CommunityTypeOptions"; import CommunityNameSection from "./CommunityNameSection"; +import { + createCommunitySchema, + CreateCommunityInput, +} from "@/schema/community"; const COMMUNITY_TYPE_OPTIONS = [ { @@ -55,33 +53,32 @@ type CreateCommunityModalProps = { handleClose: () => void; }; -/** - * Modal for creating communities with name validation and privacy selection. - * @param props - Open state and close handler provided by the parent. - * @returns Dialog that submits creation through `useCreateCommunity`. - */ const CreateCommunityModal: React.FC = ({ open, handleClose, }) => { - const communityNameLengthLimit = 25; // community names are 25 characters long - const [communityName, setCommunityName] = useState(""); - const [charRemaining, setCharRemaining] = useState(communityNameLengthLimit); - const [communityType, setCommunityType] = useState("public"); - const { createCommunity, loading, error, setError } = useCreateCommunity(); - - const handleChange = (event: React.ChangeEvent) => { - if (event.target.value.length > communityNameLengthLimit) return; // community is not created if the name is above the limit - setCommunityName(event.target.value); // updates the state of `communityName` - setCharRemaining(communityNameLengthLimit - event.target.value.length); // computing remaining characters for community names - }; + const { createCommunity, loading } = useCreateCommunity(); + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(createCommunitySchema), + defaultValues: { + type: "public", + name: "", + }, + mode: "onChange", + }); - const onCommunityTypeChange = (value: string) => { - setCommunityType(value); - }; + const communityName = watch("name"); + const communityType = watch("type"); + const charRemaining = 21 - (communityName?.length || 0); - const handleCreateCommunity = async () => { - const success = await createCommunity(communityName, communityType); + const onSubmit = async (data: CreateCommunityInput) => { + const success = await createCommunity(data.name, data.type); if (success) { handleClose(); } @@ -112,24 +109,27 @@ const CreateCommunityModal: React.FC = ({ flexDirection="column" padding="10px 0px" > - - - - - Community Type - - - + - + + + + Community Type + + + + setValue("type", value as any) + } + /> + + @@ -149,7 +149,7 @@ const CreateCommunityModal: React.FC = ({ {isEditing ? ( - ) : ( diff --git a/components/modal/profile/UserInfoSection.tsx b/components/modal/profile/UserInfoSection.tsx index a2d3b01..1bdb8c7 100644 --- a/components/modal/profile/UserInfoSection.tsx +++ b/components/modal/profile/UserInfoSection.tsx @@ -1,27 +1,29 @@ import { Flex, Input, Text } from "@chakra-ui/react"; import { User } from "firebase/auth"; import React from "react"; +import { UseFormRegister, FieldErrors } from "react-hook-form"; +import { EditProfileInput } from "@/schema/profile"; type UserInfoSectionProps = { user: User | null | undefined; isEditing: boolean; - userName: string; - handleNameChange: (event: React.ChangeEvent) => void; + register: UseFormRegister; + errors: FieldErrors; }; /** * Profile info block that toggles between read-only and editable username. * @param user - Firebase user for email and current display name. * @param isEditing - Whether to show the input. - * @param userName - Controlled input value for the name. - * @param handleNameChange - Change handler for the name field. + * @param register - React Hook Form register function. + * @param errors - React Hook Form errors object. * @returns Email and name display with optional edit input. */ const UserInfoSection: React.FC = ({ user, isEditing, - userName, - handleNameChange, + register, + errors, }) => { return ( <> @@ -52,7 +54,7 @@ const UserInfoSection: React.FC = ({ )} {isEditing && ( - + = ({ User Name + {errors.displayName && ( + + {errors.displayName.message} + + )} )} diff --git a/components/posts/comments/CommentInput.tsx b/components/posts/comments/CommentInput.tsx index 45d99f3..f26b88a 100644 --- a/components/posts/comments/CommentInput.tsx +++ b/components/posts/comments/CommentInput.tsx @@ -4,53 +4,46 @@ import { Flex, Textarea, Button, Text, Stack, Icon } from "@chakra-ui/react"; import { LuSend, LuTrash } from "react-icons/lu"; import { User } from "firebase/auth"; import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + commentSchema, + CommentInput as CommentInputType, +} from "@/schema/comment"; -/** - * Required props for CommentInput component - * @param {string} commentText - text of the comment - * @param {setCommentText} setCommentText - function to set the comment text - * @param {User} user - User object from firebase - * @param {boolean} createLoading - is the comment being created - * @param {onCreateComment} onCreateComment - function to handle creating comment - */ type CommentInputProps = { - commentText: string; - setCommentText: (value: string) => void; user?: User | null; createLoading: boolean; onCreateComment: (commentText: string) => void; }; -/** - * Input box for creating a comment by inputting text. - * The component displays: - * - Textarea for inputting comment text - * - Button for creating the comment - * - * If the user is not logged in, the component displays: - * - Text prompting the user to log in or sign up - * - AuthButtons component - * - * @param {string} commentText - text of the comment - * @param {setCommentText} setCommentText - function to set the comment text - * @param {User} user - User object from firebase - * @param {boolean} createLoading - is the comment being created - * @param {onCreateComment} onCreateComment - function to handle creating comment - * - * @returns {React.FC} - input box for creating a comment - */ const CommentInput: React.FC = ({ - commentText, - setCommentText, user, createLoading, onCreateComment, }) => { const [isProfileModalOpen, setProfileModalOpen] = useState(false); + const { + register, + handleSubmit, + reset, + watch, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(commentSchema), + mode: "onChange", + }); + + const commentText = watch("text"); + + const onSubmit = (data: CommentInputType) => { + onCreateComment(data.text); + reset(); + }; + return ( {user ? ( - // If the user is logged in, display the comment input box <> setProfileModalOpen(false)} @@ -74,70 +67,75 @@ const CommentInput: React.FC = ({ - -