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:
@@ -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;
|
||||||
@@ -1,104 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { TrashIcon } from "@/assets/icons";
|
import SectionStep from "./SectionStep";
|
||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
import useFeaturesStepPresenter, {
|
||||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
IFeaturesStepFields,
|
||||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
} from "./useFeaturesStepPresenter";
|
||||||
import { FormErrorMessages } from "@/constants/Texts";
|
|
||||||
import useFeaturesStepPresenter from "./useFeaturesStepPresenter";
|
|
||||||
|
|
||||||
const FeaturesStep = () => {
|
const FeaturesStep = () => {
|
||||||
const { errors, handleSubmit, onSubmit, register, fields, append, remove } =
|
const {
|
||||||
useFeaturesStepPresenter();
|
errors,
|
||||||
|
handleSubmit,
|
||||||
|
onSubmit,
|
||||||
|
register,
|
||||||
|
fields,
|
||||||
|
append,
|
||||||
|
remove,
|
||||||
|
control,
|
||||||
|
} = useFeaturesStepPresenter();
|
||||||
return (
|
return (
|
||||||
<form
|
<SectionStep<IFeaturesStepFields>
|
||||||
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7"
|
title="Features"
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
fields={fields}
|
||||||
>
|
handleSubmit={handleSubmit}
|
||||||
<div className="flex flex-col gap-9">
|
onSubmit={onSubmit}
|
||||||
<ShowcaseSection
|
register={register}
|
||||||
title="Features Section"
|
errors={errors}
|
||||||
className="flex flex-col gap-5"
|
append={append}
|
||||||
>
|
remove={remove}
|
||||||
<InputGroup
|
control={control}
|
||||||
{...register("section_title", {
|
fieldName="features"
|
||||||
required: { message: FormErrorMessages.required, value: true },
|
defaultValues={{ icon: "", title: "", description: "" }}
|
||||||
})}
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -4,6 +4,7 @@ import Stepper, { Step } from "@/components/ui/Stepper";
|
|||||||
import { BreadcrumbItem } from "@/types/shared";
|
import { BreadcrumbItem } from "@/types/shared";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ShowcaseSection } from "../../components/Layouts/showcase-section";
|
import { ShowcaseSection } from "../../components/Layouts/showcase-section";
|
||||||
|
import ChallengesStep from "./_components/ChallengesStep";
|
||||||
import ChartStep from "./_components/ChartStep";
|
import ChartStep from "./_components/ChartStep";
|
||||||
import FeaturesStep from "./_components/FeaturesStep";
|
import FeaturesStep from "./_components/FeaturesStep";
|
||||||
import HeroStep from "./_components/HeroStep";
|
import HeroStep from "./_components/HeroStep";
|
||||||
@@ -37,9 +38,14 @@ const Marketing = () => {
|
|||||||
content: <FeaturesStep />,
|
content: <FeaturesStep />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Review",
|
title: "Challenges",
|
||||||
description: "Review and confirm",
|
description: "Create challenges section",
|
||||||
content: <div>step 4</div>,
|
content: <ChallengesStep />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Benefits",
|
||||||
|
description: "create benefits section",
|
||||||
|
content: <ChallengesStep />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user