chore(env): update application version to 2.2.8 in production environment

feat(marketing): enhance step components with validity reporting

- Added onValidityChange prop to multiple step components (BenefitsStep, ChallengesStep, ChartStep, ContactsStep, FeaturesStep, FooterStep, HeroStep) to handle form validity.
- Updated respective presenter hooks to utilize the new onValidityChange functionality for real-time validity reporting.
- Enhanced MarketingForm to manage step validity and disable the next button based on current step validity.
This commit is contained in:
AmirReza Jamali
2026-06-06 15:39:03 +03:30
parent 5d01d66ea3
commit 88f223bf08
19 changed files with 152 additions and 32 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
NEXT_PUBLIC_APP_VERSION=2.2.7
NEXT_PUBLIC_APP_VERSION=2.2.8
NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com
@@ -7,9 +7,10 @@ import useBenefitsStepPresenter, {
interface BenefitsStepProps {
initialData?: IBenefitsStepFields;
onValidityChange?: (valid: boolean) => void;
}
const BenefitsStep = forwardRef<unknown, BenefitsStepProps>(({ initialData }, ref) => {
const BenefitsStep = forwardRef<unknown, BenefitsStepProps>(({ initialData, onValidityChange }, ref) => {
const {
errors,
handleSubmit,
@@ -19,7 +20,7 @@ const BenefitsStep = forwardRef<unknown, BenefitsStepProps>(({ initialData }, re
append,
remove,
control,
} = useBenefitsStepPresenter(initialData, ref);
} = useBenefitsStepPresenter(initialData, ref, onValidityChange);
return (
<SectionStep<IBenefitsStepFields>
@@ -7,10 +7,11 @@ import useChallengesStepPresenter, {
interface ChallengesStepProps {
initialData?: IChallengesStepFields;
onValidityChange?: (valid: boolean) => void;
}
const ChallengesStep = forwardRef<unknown, ChallengesStepProps>(
({ initialData }, ref) => {
({ initialData, onValidityChange }, ref) => {
const {
errors,
handleSubmit,
@@ -20,7 +21,7 @@ const ChallengesStep = forwardRef<unknown, ChallengesStepProps>(
append,
remove,
control,
} = useChallengesStepPresenter(initialData, ref);
} = useChallengesStepPresenter(initialData, ref, onValidityChange);
return (
<SectionStep<IChallengesStepFields>
+3 -2
View File
@@ -10,10 +10,11 @@ import useChartStepPresenter from "./useChartStepPresenter";
interface ChartStepProps {
initialData?: any;
onValidityChange?: (valid: boolean) => void;
}
const ChartStep = forwardRef<unknown, ChartStepProps>(
({ initialData }, ref) => {
({ initialData, onValidityChange }, ref) => {
const {
errors,
handleSubmit,
@@ -23,7 +24,7 @@ const ChartStep = forwardRef<unknown, ChartStepProps>(
append,
remove,
watch,
} = useChartStepPresenter(initialData, ref);
} = useChartStepPresenter(initialData, ref, onValidityChange);
const chartTitle = watch("title");
const chartData = watch("data");
@@ -9,12 +9,13 @@ import useContactsStepPresenter from "./useContactsStepPresenter";
interface ContactsStepProps {
initialData?: any;
onValidityChange?: (valid: boolean) => void;
}
const ContactsStep = forwardRef<unknown, ContactsStepProps>(
({ initialData }, ref) => {
({ initialData, onValidityChange }, ref) => {
const { errors, handleSubmit, onSubmit, register, fields } =
useContactsStepPresenter(initialData, ref);
useContactsStepPresenter(initialData, ref, onValidityChange);
return (
<form
@@ -7,10 +7,11 @@ import useFeaturesStepPresenter, {
interface FeaturesStepProps {
initialData?: IFeaturesStepFields;
onValidityChange?: (valid: boolean) => void;
}
const FeaturesStep = forwardRef<unknown, FeaturesStepProps>(
({ initialData }, ref) => {
({ initialData, onValidityChange }, ref) => {
const {
errors,
handleSubmit,
@@ -20,7 +21,7 @@ const FeaturesStep = forwardRef<unknown, FeaturesStepProps>(
append,
remove,
control,
} = useFeaturesStepPresenter(initialData, ref);
} = useFeaturesStepPresenter(initialData, ref, onValidityChange);
return (
<SectionStep<IFeaturesStepFields>
+3 -1
View File
@@ -8,13 +8,15 @@ import useFooterStepPresenter from "./useFooterStepPresenter";
interface FooterStepProps {
initialData?: any;
onValidityChange?: (valid: boolean) => void;
}
const FooterStep = forwardRef<unknown, FooterStepProps>(
({ initialData }, ref) => {
({ initialData, onValidityChange }, ref) => {
const { errors, handleSubmit, onSubmit, register } = useFooterStepPresenter(
initialData,
ref,
onValidityChange,
);
return (
+3 -1
View File
@@ -9,12 +9,14 @@ import useHeroStepPresenter from "./useHeroStepPresenter";
interface HeroStepProps {
initialData?: any;
onValidityChange?: (valid: boolean) => void;
}
const HeroStep = forwardRef<unknown, HeroStepProps>(({ initialData }, ref) => {
const HeroStep = forwardRef<unknown, HeroStepProps>(({ initialData, onValidityChange }, ref) => {
const { errors, handleSubmit, onSubmit, register } = useHeroStepPresenter(
initialData,
ref,
onValidityChange,
);
return (
@@ -43,6 +43,8 @@ const MarketingForm = ({
watchPageType,
pageTypeErrors,
isCompany,
isCurrentStepValid,
stepValidityHandlers,
setValue,
heroRef,
chartRef,
@@ -177,28 +179,53 @@ const MarketingForm = ({
</div>
<div style={{ display: step === 0 ? "block" : "none" }}>
<HeroStep ref={heroRef} initialData={formData.hero} />
<HeroStep
ref={heroRef}
initialData={formData.hero}
onValidityChange={stepValidityHandlers[0]}
/>
</div>
<div style={{ display: step === 1 ? "block" : "none" }}>
<ChartStep ref={chartRef} initialData={formData.chart} />
<ChartStep
ref={chartRef}
initialData={formData.chart}
onValidityChange={stepValidityHandlers[1]}
/>
</div>
<div style={{ display: step === 2 ? "block" : "none" }}>
<FeaturesStep ref={featuresRef} initialData={formData.features} />
<FeaturesStep
ref={featuresRef}
initialData={formData.features}
onValidityChange={stepValidityHandlers[2]}
/>
</div>
<div style={{ display: step === 3 ? "block" : "none" }}>
<ChallengesStep
ref={challengesRef}
initialData={formData.challenges}
onValidityChange={stepValidityHandlers[3]}
/>
</div>
<div style={{ display: step === 4 ? "block" : "none" }}>
<BenefitsStep ref={benefitsRef} initialData={formData.advantages} />
<BenefitsStep
ref={benefitsRef}
initialData={formData.advantages}
onValidityChange={stepValidityHandlers[4]}
/>
</div>
<div style={{ display: step === 5 ? "block" : "none" }}>
<ContactsStep ref={contactsRef} initialData={formData.contact} />
<ContactsStep
ref={contactsRef}
initialData={formData.contact}
onValidityChange={stepValidityHandlers[5]}
/>
</div>
<div style={{ display: step === 6 ? "block" : "none" }}>
<FooterStep ref={footerRef} initialData={formData.footer} />
<FooterStep
ref={footerRef}
initialData={formData.footer}
onValidityChange={stepValidityHandlers[6]}
/>
</div>
<Stepper
@@ -207,6 +234,7 @@ const MarketingForm = ({
onStepChange={handleStepChange}
onSubmit={handleSubmitAll}
isSubmitting={isPending}
nextDisabled={!isCurrentStepValid}
/>
</ShowcaseSection>
</>
@@ -1,6 +1,7 @@
"use client";
import { ForwardedRef, useImperativeHandle } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
export interface IBenefitsStepFields {
title: string;
@@ -15,21 +16,25 @@ export interface IBenefitsStepFields {
const useBenefitsStepPresenter = (
initialData?: IBenefitsStepFields,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
formState: { errors, isValid },
getValues,
trigger,
} = useForm<IBenefitsStepFields>({
defaultValues: initialData || {
cards: [{ icon: "", title: "", description: "" }],
},
mode: "onChange",
});
useReportStepValidity(isValid, onValidityChange);
const { fields, append, remove } = useFieldArray({
control,
name: "cards",
@@ -1,6 +1,7 @@
"use client";
import { ForwardedRef, useImperativeHandle } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
export interface IChallengesStepFields {
title: string;
@@ -15,21 +16,25 @@ export interface IChallengesStepFields {
const useChallengesStepPresenter = (
initialData?: IChallengesStepFields,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
formState: { errors, isValid },
getValues,
trigger,
} = useForm<IChallengesStepFields>({
defaultValues: initialData || {
cards: [{ icon: "", title: "", description: "" }],
},
mode: "onChange",
});
useReportStepValidity(isValid, onValidityChange);
const { fields, append, remove } = useFieldArray({
control,
name: "cards",
@@ -1,6 +1,7 @@
"use client";
import { ForwardedRef, useImperativeHandle } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
export interface IChartStepFields {
title: string;
@@ -11,21 +12,25 @@ export interface IChartStepFields {
const useChartStepPresenter = (
initialData?: IChartStepFields,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
formState: { errors, isValid },
getValues,
trigger,
} = useForm<IChartStepFields>({
defaultValues: initialData || {
data: [{ key: "", value: 0 }],
},
mode: "onChange",
});
useReportStepValidity(isValid, onValidityChange);
const { fields, append, remove } = useFieldArray({
control,
name: "data",
@@ -1,6 +1,7 @@
"use client";
import { ForwardedRef, useImperativeHandle } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
export interface IContactsStepFields {
title: string;
@@ -17,15 +18,17 @@ export interface IContactsStepFields {
const useContactsStepPresenter = (
initialData?: IContactsStepFields,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
control,
getValues,
formState: { errors },
formState: { errors, isValid },
trigger,
} = useForm<IContactsStepFields>({
mode: "onChange",
defaultValues: initialData || {
fields: [
{
@@ -51,6 +54,8 @@ const useContactsStepPresenter = (
name: "fields",
});
useReportStepValidity(isValid, onValidityChange);
const onSubmit = (data: IContactsStepFields) => {
console.log(data);
};
@@ -1,6 +1,7 @@
"use client";
import { ForwardedRef, useImperativeHandle } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
export interface IFeaturesStepFields {
title: string;
@@ -15,21 +16,25 @@ export interface IFeaturesStepFields {
const useFeaturesStepPresenter = (
initialData?: IFeaturesStepFields,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
formState: { errors, isValid },
getValues,
trigger,
} = useForm<IFeaturesStepFields>({
defaultValues: initialData || {
cards: [{ icon: "", title: "", description: "" }],
},
mode: "onChange",
});
useReportStepValidity(isValid, onValidityChange);
const { fields, append, remove } = useFieldArray({
control,
name: "cards",
@@ -1,5 +1,6 @@
import { ForwardedRef, useImperativeHandle } from "react";
import { useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
interface FooterFormValues {
email: string;
@@ -12,17 +13,21 @@ interface FooterFormValues {
const useFooterStepPresenter = (
initialData?: FooterFormValues,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
formState: { errors },
formState: { errors, isValid },
getValues,
trigger,
} = useForm<FooterFormValues>({
defaultValues: initialData,
mode: "onChange",
});
useReportStepValidity(isValid, onValidityChange);
const onSubmit = (data: FooterFormValues) => {
console.log(data);
};
@@ -1,6 +1,7 @@
"use client";
import { ForwardedRef, useImperativeHandle } from "react";
import { useForm } from "react-hook-form";
import useReportStepValidity from "./useReportStepValidity";
export interface IHeroStepFields {
title: string;
@@ -13,17 +14,21 @@ export interface IHeroStepFields {
const useHeroStepPresenter = (
initialData?: IHeroStepFields,
ref?: ForwardedRef<unknown>,
onValidityChange?: (valid: boolean) => void,
) => {
const {
register,
handleSubmit,
formState: { errors },
formState: { errors, isValid },
getValues,
trigger,
} = useForm<IHeroStepFields>({
defaultValues: initialData,
mode: "onChange",
});
useReportStepValidity(isValid, onValidityChange);
const onSubmit = (data: IHeroStepFields) => {
console.log(data);
};
@@ -2,7 +2,7 @@
import { Step } from "@/components/ui/Stepper";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useForm } from "react-hook-form";
@@ -13,6 +13,9 @@ interface IPageTypeFields {
country?: string;
key: string;
}
const TOTAL_STEPS = 7;
const useMarketingForm = (
initialData: any,
onSubmit: (data: any) => void,
@@ -20,6 +23,9 @@ const useMarketingForm = (
const [step, setStep] = useState(0);
const [formData, setFormData] = useState<any>(initialData || {});
const [stepValidity, setStepValidity] = useState<boolean[]>(() =>
new Array(TOTAL_STEPS).fill(false),
);
const heroRef = useRef<any>(null);
const chartRef = useRef<any>(null);
const featuresRef = useRef<any>(null);
@@ -31,12 +37,13 @@ const useMarketingForm = (
const {
register: registerPageType,
watch: watchPageType,
formState: { errors: pageTypeErrors },
formState: { errors: pageTypeErrors, isValid: isPageTypeValid },
getValues: getPageTypeValues,
setValue,
reset,
trigger: triggerPageType,
} = useForm<IPageTypeFields>({
mode: "onChange",
defaultValues: {
type: "company",
language: "en",
@@ -46,6 +53,25 @@ const useMarketingForm = (
},
});
// Stable per-step callbacks so each step form can report its live validity
// without recreating handlers (which would re-fire the reporting effect).
const stepValidityHandlers = useMemo(
() =>
Array.from(
{ length: TOTAL_STEPS },
(_, index) => (valid: boolean) =>
setStepValidity((prev) => {
if (prev[index] === valid) return prev;
const next = [...prev];
next[index] = valid;
return next;
}),
),
[],
);
const isCurrentStepValid = isPageTypeValid && Boolean(stepValidity[step]);
useEffect(() => {
if (initialData) {
const formValues = {
@@ -245,6 +271,8 @@ const useMarketingForm = (
watchPageType,
pageTypeErrors,
isCompany,
isCurrentStepValid,
stepValidityHandlers,
setValue,
heroRef,
chartRef,
@@ -0,0 +1,17 @@
"use client";
import { useEffect } from "react";
/**
* Bridges a step form's reactive `isValid` state up to the parent stepper.
* Shared across every step presenter so the reporting logic lives in one place.
*/
const useReportStepValidity = (
isValid: boolean,
onValidityChange?: (valid: boolean) => void,
) => {
useEffect(() => {
onValidityChange?.(isValid);
}, [isValid, onValidityChange]);
};
export default useReportStepValidity;
+7 -4
View File
@@ -22,6 +22,7 @@ interface StepperProps {
onStepChange?: (step: number) => void;
onSubmit?: () => void;
isSubmitting?: boolean;
nextDisabled?: boolean;
}
const Stepper: React.FC<StepperProps> = ({
@@ -30,6 +31,7 @@ const Stepper: React.FC<StepperProps> = ({
onStepChange,
onSubmit,
isSubmitting = false,
nextDisabled = false,
}) => {
const [internalStep, setInternalStep] = useState(0);
const currentStep =
@@ -213,6 +215,7 @@ const Stepper: React.FC<StepperProps> = ({
};
const nextStep = () => {
if (nextDisabled) return;
if (currentStep === steps.length - 1) {
if (onSubmit) onSubmit();
} else if (currentStep < steps.length - 1) {
@@ -425,20 +428,20 @@ const Stepper: React.FC<StepperProps> = ({
onClick={nextStep}
onMouseEnter={() => handleButtonHover(nextBtnRef.current, true)}
onMouseLeave={() => handleButtonHover(nextBtnRef.current, false)}
disabled={isSubmitting}
disabled={isSubmitting || nextDisabled}
className={`group relative inline-flex items-center gap-2 overflow-hidden rounded-xl px-6 py-2.5 text-sm font-semibold text-white transition-all ${
isSubmitting
isSubmitting || nextDisabled
? "cursor-not-allowed bg-gray-4"
: "bg-gradient-to-r from-primary via-blue to-primary bg-[length:200%_100%] shadow-lg shadow-primary/30 hover:shadow-primary/50"
}`}
style={
!isSubmitting
!isSubmitting && !nextDisabled
? { animation: "stepperShimmer 3s linear infinite" }
: undefined
}
>
{/* Glow halo for submit */}
{isLastStep && !isSubmitting && (
{isLastStep && !isSubmitting && !nextDisabled && (
<span
ref={submitGlowRef}
aria-hidden