From 88f223bf087bc3621341bfef1bc488822a2b8e79 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 6 Jun 2026 15:39:03 +0330 Subject: [PATCH] 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. --- .env.production | 2 +- .../marketing/_components/BenefitsStep.tsx | 5 ++- .../marketing/_components/ChallengesStep.tsx | 5 ++- src/app/marketing/_components/ChartStep.tsx | 5 ++- .../marketing/_components/ContactsStep.tsx | 5 ++- .../marketing/_components/FeaturesStep.tsx | 5 ++- src/app/marketing/_components/FooterStep.tsx | 4 +- src/app/marketing/_components/HeroStep.tsx | 4 +- .../marketing/_components/MarketingForm.tsx | 40 ++++++++++++++++--- .../_components/useBenefitsStepPresenter.ts | 7 +++- .../_components/useChallengesStepPresenter.ts | 7 +++- .../_components/useChartStepPresenter.ts | 7 +++- .../_components/useContactsStepPresenter.ts | 7 +++- .../_components/useFeaturesStepPresenter.ts | 7 +++- .../_components/useFooterStepPresenter.ts | 7 +++- .../_components/useHeroStepPresenter.ts | 7 +++- .../marketing/_components/useMarketingForm.ts | 32 ++++++++++++++- .../_components/useReportStepValidity.ts | 17 ++++++++ src/components/ui/Stepper.tsx | 11 +++-- 19 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 src/app/marketing/_components/useReportStepValidity.ts diff --git a/.env.production b/.env.production index f09122a..12d1926 100644 --- a/.env.production +++ b/.env.production @@ -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 diff --git a/src/app/marketing/_components/BenefitsStep.tsx b/src/app/marketing/_components/BenefitsStep.tsx index e88574f..4898f64 100644 --- a/src/app/marketing/_components/BenefitsStep.tsx +++ b/src/app/marketing/_components/BenefitsStep.tsx @@ -7,9 +7,10 @@ import useBenefitsStepPresenter, { interface BenefitsStepProps { initialData?: IBenefitsStepFields; + onValidityChange?: (valid: boolean) => void; } -const BenefitsStep = forwardRef(({ initialData }, ref) => { +const BenefitsStep = forwardRef(({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, @@ -19,7 +20,7 @@ const BenefitsStep = forwardRef(({ initialData }, re append, remove, control, - } = useBenefitsStepPresenter(initialData, ref); + } = useBenefitsStepPresenter(initialData, ref, onValidityChange); return ( diff --git a/src/app/marketing/_components/ChallengesStep.tsx b/src/app/marketing/_components/ChallengesStep.tsx index 2370116..29df20d 100644 --- a/src/app/marketing/_components/ChallengesStep.tsx +++ b/src/app/marketing/_components/ChallengesStep.tsx @@ -7,10 +7,11 @@ import useChallengesStepPresenter, { interface ChallengesStepProps { initialData?: IChallengesStepFields; + onValidityChange?: (valid: boolean) => void; } const ChallengesStep = forwardRef( - ({ initialData }, ref) => { + ({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, @@ -20,7 +21,7 @@ const ChallengesStep = forwardRef( append, remove, control, - } = useChallengesStepPresenter(initialData, ref); + } = useChallengesStepPresenter(initialData, ref, onValidityChange); return ( diff --git a/src/app/marketing/_components/ChartStep.tsx b/src/app/marketing/_components/ChartStep.tsx index 2eb43f3..89b3cc0 100644 --- a/src/app/marketing/_components/ChartStep.tsx +++ b/src/app/marketing/_components/ChartStep.tsx @@ -10,10 +10,11 @@ import useChartStepPresenter from "./useChartStepPresenter"; interface ChartStepProps { initialData?: any; + onValidityChange?: (valid: boolean) => void; } const ChartStep = forwardRef( - ({ initialData }, ref) => { + ({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, @@ -23,7 +24,7 @@ const ChartStep = forwardRef( append, remove, watch, - } = useChartStepPresenter(initialData, ref); + } = useChartStepPresenter(initialData, ref, onValidityChange); const chartTitle = watch("title"); const chartData = watch("data"); diff --git a/src/app/marketing/_components/ContactsStep.tsx b/src/app/marketing/_components/ContactsStep.tsx index eedf84c..6514ace 100644 --- a/src/app/marketing/_components/ContactsStep.tsx +++ b/src/app/marketing/_components/ContactsStep.tsx @@ -9,12 +9,13 @@ import useContactsStepPresenter from "./useContactsStepPresenter"; interface ContactsStepProps { initialData?: any; + onValidityChange?: (valid: boolean) => void; } const ContactsStep = forwardRef( - ({ initialData }, ref) => { + ({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, onSubmit, register, fields } = - useContactsStepPresenter(initialData, ref); + useContactsStepPresenter(initialData, ref, onValidityChange); return (
void; } const FeaturesStep = forwardRef( - ({ initialData }, ref) => { + ({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, @@ -20,7 +21,7 @@ const FeaturesStep = forwardRef( append, remove, control, - } = useFeaturesStepPresenter(initialData, ref); + } = useFeaturesStepPresenter(initialData, ref, onValidityChange); return ( diff --git a/src/app/marketing/_components/FooterStep.tsx b/src/app/marketing/_components/FooterStep.tsx index a6885b1..965254d 100644 --- a/src/app/marketing/_components/FooterStep.tsx +++ b/src/app/marketing/_components/FooterStep.tsx @@ -8,13 +8,15 @@ import useFooterStepPresenter from "./useFooterStepPresenter"; interface FooterStepProps { initialData?: any; + onValidityChange?: (valid: boolean) => void; } const FooterStep = forwardRef( - ({ initialData }, ref) => { + ({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, onSubmit, register } = useFooterStepPresenter( initialData, ref, + onValidityChange, ); return ( diff --git a/src/app/marketing/_components/HeroStep.tsx b/src/app/marketing/_components/HeroStep.tsx index 7df278b..4e6dbf9 100644 --- a/src/app/marketing/_components/HeroStep.tsx +++ b/src/app/marketing/_components/HeroStep.tsx @@ -9,12 +9,14 @@ import useHeroStepPresenter from "./useHeroStepPresenter"; interface HeroStepProps { initialData?: any; + onValidityChange?: (valid: boolean) => void; } -const HeroStep = forwardRef(({ initialData }, ref) => { +const HeroStep = forwardRef(({ initialData, onValidityChange }, ref) => { const { errors, handleSubmit, onSubmit, register } = useHeroStepPresenter( initialData, ref, + onValidityChange, ); return ( diff --git a/src/app/marketing/_components/MarketingForm.tsx b/src/app/marketing/_components/MarketingForm.tsx index b3a9ae8..11b59f1 100644 --- a/src/app/marketing/_components/MarketingForm.tsx +++ b/src/app/marketing/_components/MarketingForm.tsx @@ -43,6 +43,8 @@ const MarketingForm = ({ watchPageType, pageTypeErrors, isCompany, + isCurrentStepValid, + stepValidityHandlers, setValue, heroRef, chartRef, @@ -177,28 +179,53 @@ const MarketingForm = ({
- +
- +
- +
- +
- +
- +
diff --git a/src/app/marketing/_components/useBenefitsStepPresenter.ts b/src/app/marketing/_components/useBenefitsStepPresenter.ts index f35d407..54b6fef 100644 --- a/src/app/marketing/_components/useBenefitsStepPresenter.ts +++ b/src/app/marketing/_components/useBenefitsStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, control, watch, - formState: { errors }, + formState: { errors, isValid }, getValues, trigger, } = useForm({ defaultValues: initialData || { cards: [{ icon: "", title: "", description: "" }], }, + mode: "onChange", }); + useReportStepValidity(isValid, onValidityChange); + const { fields, append, remove } = useFieldArray({ control, name: "cards", diff --git a/src/app/marketing/_components/useChallengesStepPresenter.ts b/src/app/marketing/_components/useChallengesStepPresenter.ts index af9a5ab..6709f4a 100644 --- a/src/app/marketing/_components/useChallengesStepPresenter.ts +++ b/src/app/marketing/_components/useChallengesStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, control, watch, - formState: { errors }, + formState: { errors, isValid }, getValues, trigger, } = useForm({ defaultValues: initialData || { cards: [{ icon: "", title: "", description: "" }], }, + mode: "onChange", }); + useReportStepValidity(isValid, onValidityChange); + const { fields, append, remove } = useFieldArray({ control, name: "cards", diff --git a/src/app/marketing/_components/useChartStepPresenter.ts b/src/app/marketing/_components/useChartStepPresenter.ts index 7be520b..492d1e7 100644 --- a/src/app/marketing/_components/useChartStepPresenter.ts +++ b/src/app/marketing/_components/useChartStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, control, watch, - formState: { errors }, + formState: { errors, isValid }, getValues, trigger, } = useForm({ defaultValues: initialData || { data: [{ key: "", value: 0 }], }, + mode: "onChange", }); + useReportStepValidity(isValid, onValidityChange); + const { fields, append, remove } = useFieldArray({ control, name: "data", diff --git a/src/app/marketing/_components/useContactsStepPresenter.ts b/src/app/marketing/_components/useContactsStepPresenter.ts index da274e0..37590d2 100644 --- a/src/app/marketing/_components/useContactsStepPresenter.ts +++ b/src/app/marketing/_components/useContactsStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, control, getValues, - formState: { errors }, + formState: { errors, isValid }, trigger, } = useForm({ + mode: "onChange", defaultValues: initialData || { fields: [ { @@ -51,6 +54,8 @@ const useContactsStepPresenter = ( name: "fields", }); + useReportStepValidity(isValid, onValidityChange); + const onSubmit = (data: IContactsStepFields) => { console.log(data); }; diff --git a/src/app/marketing/_components/useFeaturesStepPresenter.ts b/src/app/marketing/_components/useFeaturesStepPresenter.ts index 3114c05..13d3a0e 100644 --- a/src/app/marketing/_components/useFeaturesStepPresenter.ts +++ b/src/app/marketing/_components/useFeaturesStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, control, watch, - formState: { errors }, + formState: { errors, isValid }, getValues, trigger, } = useForm({ defaultValues: initialData || { cards: [{ icon: "", title: "", description: "" }], }, + mode: "onChange", }); + useReportStepValidity(isValid, onValidityChange); + const { fields, append, remove } = useFieldArray({ control, name: "cards", diff --git a/src/app/marketing/_components/useFooterStepPresenter.ts b/src/app/marketing/_components/useFooterStepPresenter.ts index 331686d..cc81b9a 100644 --- a/src/app/marketing/_components/useFooterStepPresenter.ts +++ b/src/app/marketing/_components/useFooterStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, - formState: { errors }, + formState: { errors, isValid }, getValues, trigger, } = useForm({ defaultValues: initialData, + mode: "onChange", }); + useReportStepValidity(isValid, onValidityChange); + const onSubmit = (data: FooterFormValues) => { console.log(data); }; diff --git a/src/app/marketing/_components/useHeroStepPresenter.ts b/src/app/marketing/_components/useHeroStepPresenter.ts index e37c70a..daa0998 100644 --- a/src/app/marketing/_components/useHeroStepPresenter.ts +++ b/src/app/marketing/_components/useHeroStepPresenter.ts @@ -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, + onValidityChange?: (valid: boolean) => void, ) => { const { register, handleSubmit, - formState: { errors }, + formState: { errors, isValid }, getValues, trigger, } = useForm({ defaultValues: initialData, + mode: "onChange", }); + useReportStepValidity(isValid, onValidityChange); + const onSubmit = (data: IHeroStepFields) => { console.log(data); }; diff --git a/src/app/marketing/_components/useMarketingForm.ts b/src/app/marketing/_components/useMarketingForm.ts index 99df223..ab5d8d6 100644 --- a/src/app/marketing/_components/useMarketingForm.ts +++ b/src/app/marketing/_components/useMarketingForm.ts @@ -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(initialData || {}); + const [stepValidity, setStepValidity] = useState(() => + new Array(TOTAL_STEPS).fill(false), + ); const heroRef = useRef(null); const chartRef = useRef(null); const featuresRef = useRef(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({ + 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, diff --git a/src/app/marketing/_components/useReportStepValidity.ts b/src/app/marketing/_components/useReportStepValidity.ts new file mode 100644 index 0000000..d7f5269 --- /dev/null +++ b/src/app/marketing/_components/useReportStepValidity.ts @@ -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; diff --git a/src/components/ui/Stepper.tsx b/src/components/ui/Stepper.tsx index 19a5ccf..0fe221a 100644 --- a/src/components/ui/Stepper.tsx +++ b/src/components/ui/Stepper.tsx @@ -22,6 +22,7 @@ interface StepperProps { onStepChange?: (step: number) => void; onSubmit?: () => void; isSubmitting?: boolean; + nextDisabled?: boolean; } const Stepper: React.FC = ({ @@ -30,6 +31,7 @@ const Stepper: React.FC = ({ onStepChange, onSubmit, isSubmitting = false, + nextDisabled = false, }) => { const [internalStep, setInternalStep] = useState(0); const currentStep = @@ -213,6 +215,7 @@ const Stepper: React.FC = ({ }; 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 = ({ 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 && (