refactor(marketing): Abstract form logic into reusable SectionStep component

Reduces code duplication in the marketing page forms by creating a generic `SectionStep` component. This new component encapsulates the shared UI and logic for sections that include a title, description, and a dynamic list of fields (e.g., feature cards, pricing points).

The `FeaturesStep` component has been refactored to use this new `SectionStep`, resulting in a much cleaner and more maintainable implementation.
This commit is contained in:
AmirReza Jamali
2025-11-05 12:44:39 +03:30
parent 90ca4054d8
commit b18aa8eb71
7 changed files with 341 additions and 100 deletions
@@ -0,0 +1,35 @@
"use client";
import SectionStep from "./SectionStep";
import useBenefitsStepPresenter, {
IBenefitsStepFields,
} from "./useBenefitsStepPresenter";
const BenefitsStep = () => {
const {
errors,
handleSubmit,
onSubmit,
register,
fields,
append,
remove,
control,
} = useBenefitsStepPresenter();
return (
<SectionStep<IBenefitsStepFields>
title="Benefits"
fields={fields}
handleSubmit={handleSubmit}
onSubmit={onSubmit}
register={register}
errors={errors}
append={append}
remove={remove}
control={control}
fieldName="benefits"
defaultValues={{ icon: "", title: "", description: "" }}
/>
);
};
export default BenefitsStep;
@@ -0,0 +1,35 @@
"use client";
import SectionStep from "./SectionStep";
import useChallengesStepPresenter, {
IChallengesStepFields,
} from "./useChallengesStepPresenter";
const ChallengesStep = () => {
const {
errors,
handleSubmit,
onSubmit,
register,
fields,
append,
remove,
control,
} = useChallengesStepPresenter();
return (
<SectionStep<IChallengesStepFields>
title="Challenges"
fields={fields}
handleSubmit={handleSubmit}
onSubmit={onSubmit}
register={register}
errors={errors}
append={append}
remove={remove}
control={control}
fieldName="challenges"
defaultValues={{ icon: "", title: "", description: "" }}
/>
);
};
export default ChallengesStep;
+27 -97
View File
@@ -1,104 +1,34 @@
"use client";
import { TrashIcon } from "@/assets/icons";
import InputGroup from "@/components/FormElements/InputGroup";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import { FormErrorMessages } from "@/constants/Texts";
import useFeaturesStepPresenter from "./useFeaturesStepPresenter";
import SectionStep from "./SectionStep";
import useFeaturesStepPresenter, {
IFeaturesStepFields,
} from "./useFeaturesStepPresenter";
const FeaturesStep = () => {
const { errors, handleSubmit, onSubmit, register, fields, append, remove } =
useFeaturesStepPresenter();
const {
errors,
handleSubmit,
onSubmit,
register,
fields,
append,
remove,
control,
} = useFeaturesStepPresenter();
return (
<form
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col gap-9">
<ShowcaseSection
title="Features Section"
className="flex flex-col gap-5"
>
<InputGroup
{...register("section_title", {
required: { message: FormErrorMessages.required, value: true },
})}
label="Section Title"
name="section_title"
required
type="text"
placeholder="Enter Section Title"
/>
{errors.section_title && (
<p className="mt-1 text-sm text-red-500">
{errors.section_title.message}
</p>
)}
<TextAreaGroup
label="Section Description"
{...register("section_description")}
name="section_description"
placeholder="Enter Section Description"
/>
</ShowcaseSection>
<ShowcaseSection title="Features cards" className="flex flex-col gap-5">
{fields.map((field, index) => (
<div
key={field.id}
className="grid grid-cols-1 gap-4 sm:grid-cols-2"
>
<InputGroup
{...register(`features.${index}.icon`, {
required: {
message: FormErrorMessages.required,
value: true,
},
})}
label="Icon"
name={`features.${index}.icon`}
required
type="text"
placeholder="Enter Icon URL or name"
/>
<InputGroup
{...register(`features.${index}.title`, {
required: {
message: FormErrorMessages.required,
value: true,
},
})}
label="Title"
name={`features.${index}.title`}
required
type="text"
placeholder="Enter Title"
/>
<TextAreaGroup
{...register(`features.${index}.description`)}
label="Description"
name={`features.${index}.description`}
placeholder="Enter Description"
className="sm:col-span-2"
/>
<button
type="button"
onClick={() => remove(index)}
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
>
<TrashIcon />
</button>
</div>
))}
<button
type="button"
className="mt-6 flex w-fit items-center justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
onClick={() => append({ icon: "", title: "", description: "" })}
>
Add point
</button>
</ShowcaseSection>
</div>
</form>
<SectionStep<IFeaturesStepFields>
title="Features"
fields={fields}
handleSubmit={handleSubmit}
onSubmit={onSubmit}
register={register}
errors={errors}
append={append}
remove={remove}
control={control}
fieldName="features"
defaultValues={{ icon: "", title: "", description: "" }}
/>
);
};
@@ -0,0 +1,137 @@
"use client";
import { TrashIcon } from "@/assets/icons";
import InputGroup from "@/components/FormElements/InputGroup";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import { FormErrorMessages } from "@/constants/Texts";
import {
Control,
FieldErrors,
FieldValues,
Path,
UseFieldArrayAppend,
UseFieldArrayRemove,
UseFormHandleSubmit,
UseFormRegister,
} from "react-hook-form";
interface IProps<T extends FieldValues> {
title: string;
fields: any[];
handleSubmit: UseFormHandleSubmit<T>;
onSubmit: (data: T) => void;
register: UseFormRegister<T>;
errors: FieldErrors<T>;
append: UseFieldArrayAppend<T, any>;
remove: UseFieldArrayRemove;
control: Control<T, any>;
fieldName: string;
defaultValues: any;
}
const SectionStep = <T extends FieldValues>({
title,
fields,
handleSubmit,
onSubmit,
register,
errors,
append,
remove,
fieldName,
defaultValues,
}: IProps<T>) => {
return (
<form
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col gap-9">
<ShowcaseSection
title={`${title} Section`}
className="flex flex-col gap-5"
>
<InputGroup
{...register("section_title" as Path<T>, {
required: { message: FormErrorMessages.required, value: true },
})}
label="Section Title"
name="section_title"
required
type="text"
placeholder="Enter Section Title"
/>
{errors.section_title && (
<p className="mt-1 text-sm text-red-500">
{errors.section_title.message as string}
</p>
)}
<TextAreaGroup
label="Section Description"
{...register("section_description" as Path<T>)}
name="section_description"
placeholder="Enter Section Description"
/>
</ShowcaseSection>
<ShowcaseSection title={`${title} cards`} className="flex flex-col gap-5">
{fields.map((field, index) => (
<div
key={field.id}
className="grid grid-cols-1 gap-4 sm:grid-cols-2"
>
<InputGroup
{...register(`${fieldName}.${index}.icon` as Path<T>, {
required: {
message: FormErrorMessages.required,
value: true,
},
})}
label="Icon"
name={`${fieldName}.${index}.icon`}
required
type="text"
placeholder="Enter Icon URL or name"
/>
<InputGroup
{...register(`${fieldName}.${index}.title` as Path<T>, {
required: {
message: FormErrorMessages.required,
value: true,
},
})}
label="Title"
name={`${fieldName}.${index}.title`}
required
type="text"
placeholder="Enter Title"
/>
<TextAreaGroup
{...register(`${fieldName}.${index}.description` as Path<T>)}
label="Description"
name={`${fieldName}.${index}.description`}
placeholder="Enter Description"
className="sm:col-span-2"
/>
<button
type="button"
onClick={() => remove(index)}
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
>
<TrashIcon />
</button>
</div>
))}
<button
type="button"
className="mt-6 flex w-fit items-center justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
onClick={() => append(defaultValues)}
>
Add point
</button>
</ShowcaseSection>
</div>
</form>
);
};
export default SectionStep;
@@ -0,0 +1,49 @@
"use client";
import { useFieldArray, useForm } from "react-hook-form";
export interface IBenefitsStepFields {
section_title: string;
section_description: string;
benefits: {
icon: string;
title: string;
description: string;
}[];
}
const useBenefitsStepPresenter = () => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
} = useForm<IBenefitsStepFields>({
defaultValues: {
benefits: [{ icon: "", title: "", description: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "benefits",
});
const onSubmit = (data: IBenefitsStepFields) => {
console.log(data);
};
return {
register,
handleSubmit,
errors,
onSubmit,
fields,
append,
remove,
control,
watch,
};
};
export default useBenefitsStepPresenter;
@@ -0,0 +1,49 @@
"use client";
import { useFieldArray, useForm } from "react-hook-form";
export interface IChallengesStepFields {
section_title: string;
section_description: string;
challenges: {
icon: string;
title: string;
description: string;
}[];
}
const useChallengesStepPresenter = () => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
} = useForm<IChallengesStepFields>({
defaultValues: {
challenges: [{ icon: "", title: "", description: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "challenges",
});
const onSubmit = (data: IChallengesStepFields) => {
console.log(data);
};
return {
register,
handleSubmit,
errors,
onSubmit,
fields,
append,
remove,
control,
watch,
};
};
export default useChallengesStepPresenter;
+9 -3
View File
@@ -4,6 +4,7 @@ import Stepper, { Step } from "@/components/ui/Stepper";
import { BreadcrumbItem } from "@/types/shared";
import { useState } from "react";
import { ShowcaseSection } from "../../components/Layouts/showcase-section";
import ChallengesStep from "./_components/ChallengesStep";
import ChartStep from "./_components/ChartStep";
import FeaturesStep from "./_components/FeaturesStep";
import HeroStep from "./_components/HeroStep";
@@ -37,9 +38,14 @@ const Marketing = () => {
content: <FeaturesStep />,
},
{
title: "Review",
description: "Review and confirm",
content: <div>step 4</div>,
title: "Challenges",
description: "Create challenges section",
content: <ChallengesStep />,
},
{
title: "Benefits",
description: "create benefits section",
content: <ChallengesStep />,
},
];
return (