refactor(forms): memoize dynamic form sections with stable ids
continuous-integration/drone/push Build is passing

Add uuid-based section keys and memoized DynamicSection to reduce re-renders, fix dashboard GSAP scoping, update logo sizing, and temporarily hide notice-type and closing-soon widgets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
AmirReza Jamali
2026-06-23 09:50:27 +03:30
parent 90a42b9fc5
commit 31d80cd6ae
12 changed files with 169 additions and 81 deletions
+1
View File
@@ -46,6 +46,7 @@
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"react-tooltip": "^5.29.1", "react-tooltip": "^5.29.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"uuid": "^14.0.1",
"zod": "^4.1.5" "zod": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
+9
View File
@@ -104,6 +104,9 @@ importers:
tailwind-merge: tailwind-merge:
specifier: ^2.6.0 specifier: ^2.6.0
version: 2.6.1 version: 2.6.1
uuid:
specifier: ^14.0.1
version: 14.0.1
zod: zod:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.3.6 version: 4.3.6
@@ -2977,6 +2980,10 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@14.0.1:
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
uuid@8.3.2: uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true hasBin: true
@@ -6218,6 +6225,8 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@14.0.1: {}
uuid@8.3.2: {} uuid@8.3.2: {}
verror@1.10.0: verror@1.10.0:
@@ -28,19 +28,25 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const root = rootRef.current;
if (!root) return;
const reduced = const reduced =
typeof window !== "undefined" && typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches; window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
if (reduced) return; if (reduced) return;
gsap.from(rootRef.current, { gsap.from(root, {
opacity: 0, opacity: 0,
y: 24, y: 24,
duration: 0.6, duration: 0.6,
ease: "power3.out", ease: "power3.out",
delay: 0.3, delay: 0.3,
}); });
gsap.from("[data-closing-row]", {
const rows = root.querySelectorAll("[data-closing-row]");
if (rows.length > 0) {
gsap.from(rows, {
x: 24, x: 24,
opacity: 0, opacity: 0,
duration: 0.45, duration: 0.45,
@@ -48,14 +54,15 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
delay: 0.55, delay: 0.55,
ease: "power2.out", ease: "power2.out",
}); });
}, rootRef); }
}, root);
return () => ctx.revert(); return () => ctx.revert();
}, [tenders.length, isLoading]); }, [tenders.length, isLoading]);
return ( return (
<div <div
ref={rootRef} ref={rootRef}
className="flex h-full flex-col overflow-hidden rounded-2xl bg-white shadow-1 ring-1 ring-stroke dark:bg-gray-dark dark:ring-stroke-dark dark:shadow-card" className="flex h-full flex-col overflow-hidden rounded-2xl bg-white shadow-1 ring-1 ring-stroke dark:bg-gray-dark dark:shadow-card dark:ring-stroke-dark"
> >
<div className="flex items-center justify-between border-b border-stroke px-5 py-4 dark:border-stroke-dark"> <div className="flex items-center justify-between border-b border-stroke px-5 py-4 dark:border-stroke-dark">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -66,7 +73,9 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
<h2 className="text-base font-bold text-dark dark:text-white"> <h2 className="text-base font-bold text-dark dark:text-white">
Closing Soon Closing Soon
</h2> </h2>
<p className="text-xs text-dark-6">Tenders with looming deadlines</p> <p className="text-xs text-dark-6">
Tenders with looming deadlines
</p>
</div> </div>
</div> </div>
<Link <Link
@@ -80,7 +89,10 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
<ul className="flex-1 divide-y divide-stroke dark:divide-stroke-dark"> <ul className="flex-1 divide-y divide-stroke dark:divide-stroke-dark">
{isLoading {isLoading
? Array.from({ length: 5 }).map((_, i) => ( ? Array.from({ length: 5 }).map((_, i) => (
<li key={i} className="flex animate-pulse items-center gap-3 px-5 py-3.5"> <li
key={i}
className="flex animate-pulse items-center gap-3 px-5 py-3.5"
>
<div className="size-10 rounded-lg bg-gray-2 dark:bg-dark-3" /> <div className="size-10 rounded-lg bg-gray-2 dark:bg-dark-3" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="h-3 w-3/4 rounded bg-gray-2 dark:bg-dark-3" /> <div className="h-3 w-3/4 rounded bg-gray-2 dark:bg-dark-3" />
@@ -53,15 +53,15 @@ export function TenderDashboard() {
<CountryDistribution countries={countries} isLoading={isPending} /> <CountryDistribution countries={countries} isLoading={isPending} />
</div> </div>
<div className="col-span-12 xl:col-span-5"> {/* <div className="col-span-12 xl:col-span-5">
<NoticeTypeBreakdown <NoticeTypeBreakdown
noticeTypes={noticeTypes} noticeTypes={noticeTypes}
isLoading={isPending} isLoading={isPending}
/> />
</div> </div> */}
<div className="col-span-12 xl:col-span-7"> {/* <div className="col-span-12 xl:col-span-7">
<ClosingSoonCard tenders={closingSoonList} isLoading={isPending} /> <ClosingSoonCard tenders={closingSoonList} isLoading={isPending} />
</div> </div> */}
<div className="col-span-12"> <div className="col-span-12">
<RecentTenders tenders={recent} isLoading={recentIsLoading} /> <RecentTenders tenders={recent} isLoading={recentIsLoading} />
@@ -45,19 +45,25 @@ export function RecentTenders({ tenders, isLoading }: Props) {
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const root = rootRef.current;
if (!root) return;
const reduced = const reduced =
typeof window !== "undefined" && typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches; window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const ctx = gsap.context(() => { const ctx = gsap.context(() => {
if (reduced) return; if (reduced) return;
gsap.from(rootRef.current, { gsap.from(root, {
opacity: 0, opacity: 0,
y: 24, y: 24,
duration: 0.6, duration: 0.6,
ease: "power3.out", ease: "power3.out",
delay: 0.35, delay: 0.35,
}); });
gsap.from("[data-recent-row]", {
const rows = root.querySelectorAll("[data-recent-row]");
if (rows.length > 0) {
gsap.from(rows, {
x: -24, x: -24,
opacity: 0, opacity: 0,
duration: 0.5, duration: 0.5,
@@ -65,14 +71,15 @@ export function RecentTenders({ tenders, isLoading }: Props) {
delay: 0.55, delay: 0.55,
ease: "power2.out", ease: "power2.out",
}); });
}, rootRef); }
}, root);
return () => ctx.revert(); return () => ctx.revert();
}, [tenders.length, isLoading]); }, [tenders.length, isLoading]);
return ( return (
<div <div
ref={rootRef} ref={rootRef}
className="flex h-full flex-col overflow-hidden rounded-2xl bg-white shadow-1 ring-1 ring-stroke dark:bg-gray-dark dark:ring-stroke-dark dark:shadow-card" className="flex h-full flex-col overflow-hidden rounded-2xl bg-white shadow-1 ring-1 ring-stroke dark:bg-gray-dark dark:shadow-card dark:ring-stroke-dark"
> >
<div className="flex items-center justify-between border-b border-stroke px-5 py-4 dark:border-stroke-dark"> <div className="flex items-center justify-between border-b border-stroke px-5 py-4 dark:border-stroke-dark">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+57 -29
View File
@@ -19,6 +19,7 @@ import type {
HTMLInputTypeAttribute, HTMLInputTypeAttribute,
ReactNode, ReactNode,
} from "react"; } from "react";
import { memo } from "react";
export interface DynamicFieldContext<TValues extends FieldValues> { export interface DynamicFieldContext<TValues extends FieldValues> {
control: Control<TValues>; control: Control<TValues>;
@@ -61,6 +62,7 @@ export type DynamicFormField<TValues extends FieldValues> =
| DynamicCustomField<TValues>; | DynamicCustomField<TValues>;
export interface DynamicFormSection<TValues extends FieldValues> { export interface DynamicFormSection<TValues extends FieldValues> {
id: string;
title?: ReactNode; title?: ReactNode;
className?: string; className?: string;
rootClassName?: string; rootClassName?: string;
@@ -90,37 +92,25 @@ const getErrorMessage = <TValues extends FieldValues>(
return typeof message === "string" ? message : undefined; return typeof message === "string" ? message : undefined;
}; };
const DynamicFormBuilder = <TValues extends FieldValues>({ interface DynamicSectionProps<TValues extends FieldValues> {
sections, section: DynamicFormSection<TValues>;
register: UseFormRegister<TValues>;
control: Control<TValues>;
errors: FieldErrors<TValues>;
footer?: ReactNode;
}
const DynamicSectionComponent = <TValues extends FieldValues>({
section,
register, register,
control, control,
errors, errors,
handleSubmit,
onSubmit,
dataCy,
className,
header,
sectionsClassName,
footer, footer,
footerPlacement = "last-section", }: DynamicSectionProps<TValues>) => (
}: DynamicFormBuilderProps<TValues>) => (
<form
className={cn("grid grid-cols-1 gap-9", className)}
onSubmit={handleSubmit(onSubmit)}
data-cy={dataCy}
noValidate
>
{header}
<div className={sectionsClassName}>
{sections.map((section, sectionIndex) => (
<ShowcaseSection <ShowcaseSection
key={sectionIndex}
title={section.title} title={section.title}
rootClassName={section.rootClassName} rootClassName={section.rootClassName}
className={cn( className={cn("grid grid-cols-1 gap-3 md:grid-cols-2", section.className)}
"grid grid-cols-1 gap-3 md:grid-cols-2",
section.className,
)}
> >
{section.fields.map((field) => { {section.fields.map((field) => {
if (field.hidden) return null; if (field.hidden) return null;
@@ -158,12 +148,50 @@ const DynamicFormBuilder = <TValues extends FieldValues>({
); );
})} })}
{footer && {footer ? <div className="md:col-span-2">{footer}</div> : null}
footerPlacement === "last-section" &&
sectionIndex === sections.length - 1 ? (
<div className="md:col-span-2">{footer}</div>
) : null}
</ShowcaseSection> </ShowcaseSection>
);
const DynamicSection = memo(
DynamicSectionComponent,
) as typeof DynamicSectionComponent;
const DynamicFormBuilder = <TValues extends FieldValues>({
sections,
register,
control,
errors,
handleSubmit,
onSubmit,
dataCy,
className,
header,
sectionsClassName,
footer,
footerPlacement = "last-section",
}: DynamicFormBuilderProps<TValues>) => (
<form
className={cn("grid grid-cols-1 gap-9", className)}
onSubmit={handleSubmit(onSubmit)}
data-cy={dataCy}
noValidate
>
{header}
<div className={sectionsClassName}>
{sections.map((section, sectionIndex) => (
<DynamicSection
key={section.id}
section={section}
register={register}
control={control}
errors={errors}
footer={
footerPlacement === "last-section" &&
sectionIndex === sections.length - 1
? footer
: undefined
}
/>
))} ))}
</div> </div>
{footerPlacement === "form" ? footer : null} {footerPlacement === "form" ? footer : null}
@@ -8,8 +8,11 @@ import FileImageUploadCard from "@/components/ui/FileImageUploadCard";
import { FormErrorMessages } from "@/constants/Texts"; import { FormErrorMessages } from "@/constants/Texts";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { Controller, type UseFormRegisterReturn } from "react-hook-form"; import { Controller, type UseFormRegisterReturn } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import type { CreateAdminFormValues } from "./createAdminForm.types"; import type { CreateAdminFormValues } from "./createAdminForm.types";
const adminSectionId = uuidv4();
interface CreateAdminFormSectionsOptions { interface CreateAdminFormSectionsOptions {
editMode?: boolean; editMode?: boolean;
passwordFieldInputType: "password" | "text"; passwordFieldInputType: "password" | "text";
@@ -36,6 +39,7 @@ export const createAdminFormSections = ({
clearSelectedProfileImage, clearSelectedProfileImage,
}: CreateAdminFormSectionsOptions): DynamicFormSection<CreateAdminFormValues>[] => [ }: CreateAdminFormSectionsOptions): DynamicFormSection<CreateAdminFormValues>[] => [
{ {
id: adminSectionId,
title: "Input Fields", title: "Input Fields",
fields: [ fields: [
{ {
@@ -2,11 +2,15 @@ import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import type { DynamicFormSection } from "@/components/forms/DynamicFormBuilder"; import type { DynamicFormSection } from "@/components/forms/DynamicFormBuilder";
import { FormErrorMessages } from "@/constants/Texts"; import { FormErrorMessages } from "@/constants/Texts";
import type { TCreateCompanyCategoryCredentials } from "@/lib/api"; import type { TCreateCompanyCategoryCredentials } from "@/lib/api";
import { v4 as uuidv4 } from "uuid";
const companyCategorySectionId = uuidv4();
export const createCompanyCategoryFormSections = ( export const createCompanyCategoryFormSections = (
editMode?: boolean, editMode?: boolean,
): DynamicFormSection<TCreateCompanyCategoryCredentials>[] => [ ): DynamicFormSection<TCreateCompanyCategoryCredentials>[] => [
{ {
id: companyCategorySectionId,
title: editMode ? "Edit Company Category" : "Create Company Category", title: editMode ? "Edit Company Category" : "Create Company Category",
className: "gap-5", className: "gap-5",
fields: [ fields: [
@@ -9,8 +9,16 @@ import { FormErrorMessages } from "@/constants/Texts";
import type { ICreateCompanyCredentials } from "@/lib/api"; import type { ICreateCompanyCredentials } from "@/lib/api";
import type { ILabelValue } from "@/types/shared"; import type { ILabelValue } from "@/types/shared";
import type { UseFormSetValue, UseFormWatch } from "react-hook-form"; import type { UseFormSetValue, UseFormWatch } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import CompanyDocuments from "./documents/CompanyDocuments"; import CompanyDocuments from "./documents/CompanyDocuments";
const companyInformationSectionId = uuidv4();
const businessInformationSectionId = uuidv4();
const settingsSectionId = uuidv4();
const addressSectionId = uuidv4();
const tagsSectionId = uuidv4();
const documentsSectionId = uuidv4();
interface CompanyFormSectionsOptions { interface CompanyFormSectionsOptions {
editMode?: boolean; editMode?: boolean;
id?: string; id?: string;
@@ -79,6 +87,7 @@ export const createCompanyFormSections = ({
setValue, setValue,
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [ }: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
{ {
id: companyInformationSectionId,
title: "Company information", title: "Company information",
rootClassName: "lg:col-span-2", rootClassName: "lg:col-span-2",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
@@ -235,6 +244,7 @@ export const createCompanyFormSections = ({
], ],
}, },
{ {
id: businessInformationSectionId,
title: "Business information", title: "Business information",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [ fields: [
@@ -276,6 +286,7 @@ export const createCompanyFormSections = ({
], ],
}, },
{ {
id: settingsSectionId,
title: "Settings", title: "Settings",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [ fields: [
@@ -323,6 +334,7 @@ export const createCompanyFormSections = ({
], ],
}, },
{ {
id: addressSectionId,
title: "Address", title: "Address",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [ fields: [
@@ -395,6 +407,7 @@ export const createCompanyFormSections = ({
], ],
}, },
{ {
id: tagsSectionId,
title: "Tags", title: "Tags",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2", className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [ fields: [
@@ -479,6 +492,7 @@ export const createCompanyFormSections = ({
], ],
}, },
{ {
id: documentsSectionId,
title: "Documents", title: "Documents",
rootClassName: "lg:col-span-2", rootClassName: "lg:col-span-2",
className: "md:grid-cols-1", className: "md:grid-cols-1",
@@ -14,6 +14,9 @@ import type { ILabelValue } from "@/types/shared";
import { getEnumAsArray } from "@/utils/shared"; import { getEnumAsArray } from "@/utils/shared";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { Controller, type UseFormWatch } from "react-hook-form"; import { Controller, type UseFormWatch } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
const customerSectionId = uuidv4();
interface CustomerFormSectionsOptions { interface CustomerFormSectionsOptions {
editMode?: boolean; editMode?: boolean;
@@ -31,6 +34,7 @@ export const createCustomerFormSections = ({
setShowPassword, setShowPassword,
}: CustomerFormSectionsOptions): DynamicFormSection<CreateCustomerCredentials>[] => [ }: CustomerFormSectionsOptions): DynamicFormSection<CreateCustomerCredentials>[] => [
{ {
id: customerSectionId,
title: "Create Customer", title: "Create Customer",
className: "gap-5", className: "gap-5",
fields: [ fields: [
@@ -8,6 +8,9 @@ import { FormErrorMessages } from "@/constants/Texts";
import type { TCreateNotificationCredentials } from "@/lib/api/types/NotificationHistory"; import type { TCreateNotificationCredentials } from "@/lib/api/types/NotificationHistory";
import type { ILabelValue } from "@/types/shared"; import type { ILabelValue } from "@/types/shared";
import type { ChangeEvent } from "react"; import type { ChangeEvent } from "react";
import { v4 as uuidv4 } from "uuid";
const notificationSectionId = uuidv4();
interface NotificationFormSectionsOptions { interface NotificationFormSectionsOptions {
recipient: ILabelValue[] | null; recipient: ILabelValue[] | null;
@@ -37,6 +40,7 @@ export const createNotificationFormSections = ({
channels, channels,
}: NotificationFormSectionsOptions): DynamicFormSection<TCreateNotificationCredentials>[] => [ }: NotificationFormSectionsOptions): DynamicFormSection<TCreateNotificationCredentials>[] => [
{ {
id: notificationSectionId,
title: "Create Notification", title: "Create Notification",
className: "gap-5", className: "gap-5",
fields: [ fields: [
+3 -2
View File
@@ -4,8 +4,9 @@ export function Logo() {
return ( return (
<Image <Image
src="/images/main-logo.svg" src="/images/main-logo.svg"
width={174} width={1217}
height={32} height={192}
className="h-auto w-[174px]"
alt="Opp lens logo" alt="Opp lens logo"
role="presentation" role="presentation"
quality={100} quality={100}