Skip to content

Commit

Permalink
feat: forms display error message
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcoEscaleira committed Mar 14, 2024
1 parent 9c4126e commit 279d289
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 106 deletions.
9 changes: 9 additions & 0 deletions src/components/Form/ErrorText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Typography } from "@material-tailwind/react";

export const ErrorText = ({ text, className }: { text: string; className?: string }) => {
return (
<Typography variant="small" color="red" className={`mt-1 font-medium ${className}`}>
{text}
</Typography>
);
};
25 changes: 25 additions & 0 deletions src/components/Form/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Input, InputProps } from "@material-tailwind/react";
import { FieldError, FieldValues, RegisterOptions, useFormContext } from "react-hook-form";
import { ErrorText } from "./ErrorText";

interface FormInputProps extends Pick<InputProps, "size" | "label" | "placeholder" | "type"> {
name: string;
registerOptions?: RegisterOptions<FieldValues, string>;
fieldError?: FieldError;
}

export const FormInput = ({ name, registerOptions, fieldError, ...props }: FormInputProps) => {
const {
register,
formState: { errors },
} = useFormContext();

const error = fieldError?.message || errors?.[name]?.message?.toString() || "";

return (
<div className="w-full">
<Input {...register(name, registerOptions)} size="lg" error={!!error} {...props} />
{error && <ErrorText text={error} />}
</div>
);
};
2 changes: 2 additions & 0 deletions src/components/Form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./FormInput";
export * from "./ErrorText";
12 changes: 6 additions & 6 deletions src/components/Quiz/OptionsFields.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Checkbox, IconButton, Input, Tooltip, Typography } from "@material-tailwind/react";
import { Checkbox, IconButton, Tooltip, Typography } from "@material-tailwind/react";
import { Plus, X } from "lucide-react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { z } from "zod";
import { FormInput } from "@components/Form";
import { quizFormSchema } from "./quizFormSchema";

interface OptionsFieldsProps {
Expand Down Expand Up @@ -34,7 +35,7 @@ export function OptionsFields({ questionIndex }: OptionsFieldsProps) {

{fields.map((field, optionIndex) => (
<div key={field.id} className="mb-4">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 mb-1">
<Typography>Option #{optionIndex + 1}</Typography>
<Tooltip content="Remove option" placement="top">
<IconButton onClick={() => remove(optionIndex)} variant="text" size="sm">
Expand All @@ -43,12 +44,11 @@ export function OptionsFields({ questionIndex }: OptionsFieldsProps) {
</Tooltip>
</div>
<div className="flex items-center gap-3">
<Input
{...register(`questions.${questionIndex}.options.${optionIndex}.text`)}
<FormInput
name={`questions.${questionIndex}.options.${optionIndex}.text`}
size="md"
label="Text"
placeholder=""
error={!!errors.questions?.[questionIndex]?.options?.[optionIndex]?.text}
fieldError={errors.questions?.[questionIndex]?.options?.[optionIndex]?.text}
/>
<Checkbox
{...register(`questions.${questionIndex}.options.${optionIndex}.correct`)}
Expand Down
11 changes: 5 additions & 6 deletions src/components/Quiz/QuestionsFields.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cloneElement } from "react";
import { IconButton, Input, Option, Select, Tooltip, Typography } from "@material-tailwind/react";
import { IconButton, Option, Select, Tooltip, Typography } from "@material-tailwind/react";
import { Plus, X } from "lucide-react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { z } from "zod";
import { FormInput } from "@components/Form";
import { QuestionTypeChip } from "@components/QuestionTypeChip/QuestionTypeChip.tsx";
import { QuestionType } from "@generated/graphql.ts";
import { OptionsFields } from "./OptionsFields";
Expand All @@ -11,7 +12,6 @@ import { quizFormSchema } from "./quizFormSchema";
export function QuestionFields() {
const {
control,
register,
getValues,
setValue,
formState: { errors },
Expand Down Expand Up @@ -48,12 +48,11 @@ export function QuestionFields() {
</div>

<div className="mb-2">
<Input
{...register(`questions.${questionIndex}.question`)}
<FormInput
name={`questions.${questionIndex}.question`}
size="md"
label="Title"
placeholder=""
error={!!errors.questions?.[questionIndex]?.question}
fieldError={errors.questions?.[questionIndex]?.question}
/>
</div>

Expand Down
43 changes: 19 additions & 24 deletions src/components/Quiz/QuizForm.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { cloneElement, useCallback, useEffect } from "react";
import { useMutation, useQuery } from "@apollo/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button, Input, Option, Select, Textarea, Typography } from "@material-tailwind/react";
import { Button, Option, Select, Textarea } from "@material-tailwind/react";
import { Loader2 } from "lucide-react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { toast } from "react-toastify";
import { useCountries } from "use-react-countries";
import { z } from "zod";
import { DifficultyChip } from "@components/DifficultyChip/DifficultyChip.tsx";
import { ErrorText, FormInput } from "@components/Form";
import { QuestionFields } from "@components/Quiz/QuestionsFields.tsx";
import { quizFormSchema } from "@components/Quiz/quizFormSchema.ts";
import { TagsInput } from "@components/TagsInput/TagsInput.tsx";
Expand Down Expand Up @@ -114,15 +115,18 @@ export function QuizForm() {
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
<Input {...register("title")} size="lg" label="Title" placeholder="A great quiz title" error={!!errors.title} />
<FormInput name="title" label="Title" placeholder="A great quiz title" />

<Textarea
{...register("description")}
size="lg"
label="Description"
placeholder=""
error={!!errors.description}
/>
<div>
<Textarea
{...register("description")}
size="lg"
label="Description"
placeholder=""
error={!!errors.description}
/>
<ErrorText text={errors.description?.message || ""} className="mt-0" />
</div>

<Select
size="lg"
Expand Down Expand Up @@ -150,7 +154,7 @@ export function QuizForm() {
))}
</Select>

<Input {...register("image")} size="lg" label="Image URL" placeholder="" error={!!errors.image} />
<FormInput name="image" label="Image URL" />

<QuestionFields />

Expand All @@ -176,31 +180,22 @@ export function QuizForm() {
))}
</Select>

<Input
{...register("timeLimit", { valueAsNumber: true, validate: value => value! > 0 })}
size="lg"
<FormInput
name="timeLimit"
registerOptions={{ valueAsNumber: true, validate: value => value! > 0 }}
label="Time limit (minutes, 0 means no limit)"
error={!!errors.timeLimit}
type="number"
/>
</div>

<TagsInput name="tags" label="Tags" />

{(mutationCreateError?.message || mutationUpdateError?.message) && (
<Typography variant="small" color="red">
{mutationCreateError?.message}
{mutationUpdateError?.message}
</Typography>
<ErrorText text={mutationCreateError?.message || mutationUpdateError?.message || ""} />
)}

{/* @ts-expect-error: not sure how to grab this type from Zod superRefine */}
{errors?.correctOption && (
<Typography variant="small" color="red">
{/* @ts-expect-error: not sure how to grab this type from Zod superRefine */}
{errors.correctOption.message}
</Typography>
)}
{errors?.correctOption && <ErrorText text={errors.correctOption.message} />}

<Button
type="submit"
Expand Down
108 changes: 38 additions & 70 deletions src/components/Register/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { toast } from "react-toastify";
import { useCountries } from "use-react-countries";
import { z } from "zod";
import { DatePicker } from "@components/DatePicker/DatePicker.tsx";
import { ErrorText, FormInput } from "@components/Form";
import { REGISTER_USER } from "@utils/queries/RegisterUser.ts";

const formSchema = z
Expand Down Expand Up @@ -51,7 +52,6 @@ export function RegisterForm() {
},
});
const {
register,
handleSubmit,
reset,
formState: { errors },
Expand Down Expand Up @@ -84,36 +84,12 @@ export function RegisterForm() {
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 py-4">
<Input
{...register("email")}
name="email"
type="email"
size="lg"
label="Email address"
placeholder="[email protected]"
error={!!errors.email}
/>
<FormInput name="email" type="email" label="Email address" placeholder="[email protected]" />

<div>
<div className="flex flex-col space-y-2 md:flex-row md:space-x-4 md:space-y-0">
<Input
{...register("password")}
name="password"
size="lg"
type="password"
label="Password"
placeholder="*******"
error={!!errors.password}
/>
<Input
{...register("passwordConfirm")}
name="passwordConfirm"
size="lg"
type="password"
label="Password confirmation"
placeholder="*******"
error={!!errors.passwordConfirm}
/>
<FormInput name="password" type="password" label="Password" placeholder="*******" />
<FormInput name="passwordConfirm" type="password" label="Password confirmation" placeholder="*******" />
</div>

<Typography variant="small" color="gray" className="mt-2 flex items-center gap-1 font-normal">
Expand All @@ -122,51 +98,43 @@ export function RegisterForm() {
</Typography>
</div>

<Input
{...register("firstName")}
name="firstName"
size="lg"
label="First name"
placeholder=""
error={!!errors.firstName}
/>
<FormInput name="firstName" label="First name" placeholder="" />

<Input
{...register("lastName")}
name="lastName"
size="lg"
label="Last name"
placeholder=""
error={!!errors.lastName}
/>
<Input name="lastName" label="Last name" />

<DatePicker
name="dateOfBirth"
label="Date of birth"
disabled={{ after: new Date() }}
error={!!errors.dateOfBirth}
/>
<div>
<DatePicker
name="dateOfBirth"
label="Date of birth"
disabled={{ after: new Date() }}
error={!!errors.dateOfBirth}
/>
<ErrorText text={errors.dateOfBirth?.message || ""} />
</div>

<Select
size="lg"
label="Select Country"
onChange={value => form.setValue("country", value || "")}
selected={element =>
element &&
cloneElement(element, {
disabled: true,
className: "flex items-center opacity-100 px-0 gap-2 pointer-events-none",
})
}
error={!!errors.country}
>
{orderedCountries().map(({ name, flags }) => (
<Option key={name} value={name} className="flex items-center gap-2">
<img src={flags.svg} alt={name} className="h-5 w-5 rounded-full object-cover" />
{name}
</Option>
))}
</Select>
<div>
<Select
size="lg"
label="Select Country"
onChange={value => form.setValue("country", value || "")}
selected={element =>
element &&
cloneElement(element, {
disabled: true,
className: "flex items-center opacity-100 px-0 gap-2 pointer-events-none",
})
}
error={!!errors.country}
>
{orderedCountries().map(({ name, flags }) => (
<Option key={name} value={name} className="flex items-center gap-2">
<img src={flags.svg} alt={name} className="h-5 w-5 rounded-full object-cover" />
{name}
</Option>
))}
</Select>
<ErrorText text={errors.country?.message || ""} />
</div>

{mutationError?.message && (
<Typography variant="small" color="red">
Expand Down

0 comments on commit 279d289

Please sign in to comment.