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-tooltip": "^5.29.1",
"tailwind-merge": "^2.6.0",
"uuid": "^14.0.1",
"zod": "^4.1.5"
},
"devDependencies": {
+9
View File
@@ -104,6 +104,9 @@ importers:
tailwind-merge:
specifier: ^2.6.0
version: 2.6.1
uuid:
specifier: ^14.0.1
version: 14.0.1
zod:
specifier: ^4.1.5
version: 4.3.6
@@ -2977,6 +2980,10 @@ packages:
util-deprecate@1.0.2:
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:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
@@ -6218,6 +6225,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@14.0.1: {}
uuid@8.3.2: {}
verror@1.10.0:
@@ -28,34 +28,41 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
const rootRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const root = rootRef.current;
if (!root) return;
const reduced =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const ctx = gsap.context(() => {
if (reduced) return;
gsap.from(rootRef.current, {
gsap.from(root, {
opacity: 0,
y: 24,
duration: 0.6,
ease: "power3.out",
delay: 0.3,
});
gsap.from("[data-closing-row]", {
x: 24,
opacity: 0,
duration: 0.45,
stagger: 0.07,
delay: 0.55,
ease: "power2.out",
});
}, rootRef);
const rows = root.querySelectorAll("[data-closing-row]");
if (rows.length > 0) {
gsap.from(rows, {
x: 24,
opacity: 0,
duration: 0.45,
stagger: 0.07,
delay: 0.55,
ease: "power2.out",
});
}
}, root);
return () => ctx.revert();
}, [tenders.length, isLoading]);
return (
<div
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 gap-2">
@@ -66,7 +73,9 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
<h2 className="text-base font-bold text-dark dark:text-white">
Closing Soon
</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>
<Link
@@ -80,7 +89,10 @@ export function ClosingSoonCard({ tenders, isLoading }: Props) {
<ul className="flex-1 divide-y divide-stroke dark:divide-stroke-dark">
{isLoading
? 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="flex-1 space-y-2">
<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} />
</div>
<div className="col-span-12 xl:col-span-5">
{/* <div className="col-span-12 xl:col-span-5">
<NoticeTypeBreakdown
noticeTypes={noticeTypes}
isLoading={isPending}
/>
</div>
<div className="col-span-12 xl:col-span-7">
</div> */}
{/* <div className="col-span-12 xl:col-span-7">
<ClosingSoonCard tenders={closingSoonList} isLoading={isPending} />
</div>
</div> */}
<div className="col-span-12">
<RecentTenders tenders={recent} isLoading={recentIsLoading} />
@@ -45,34 +45,41 @@ export function RecentTenders({ tenders, isLoading }: Props) {
const rootRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const root = rootRef.current;
if (!root) return;
const reduced =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const ctx = gsap.context(() => {
if (reduced) return;
gsap.from(rootRef.current, {
gsap.from(root, {
opacity: 0,
y: 24,
duration: 0.6,
ease: "power3.out",
delay: 0.35,
});
gsap.from("[data-recent-row]", {
x: -24,
opacity: 0,
duration: 0.5,
stagger: 0.06,
delay: 0.55,
ease: "power2.out",
});
}, rootRef);
const rows = root.querySelectorAll("[data-recent-row]");
if (rows.length > 0) {
gsap.from(rows, {
x: -24,
opacity: 0,
duration: 0.5,
stagger: 0.06,
delay: 0.55,
ease: "power2.out",
});
}
}, root);
return () => ctx.revert();
}, [tenders.length, isLoading]);
return (
<div
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 gap-2">
+79 -51
View File
@@ -19,6 +19,7 @@ import type {
HTMLInputTypeAttribute,
ReactNode,
} from "react";
import { memo } from "react";
export interface DynamicFieldContext<TValues extends FieldValues> {
control: Control<TValues>;
@@ -61,6 +62,7 @@ export type DynamicFormField<TValues extends FieldValues> =
| DynamicCustomField<TValues>;
export interface DynamicFormSection<TValues extends FieldValues> {
id: string;
title?: ReactNode;
className?: string;
rootClassName?: string;
@@ -90,6 +92,70 @@ const getErrorMessage = <TValues extends FieldValues>(
return typeof message === "string" ? message : undefined;
};
interface DynamicSectionProps<TValues extends FieldValues> {
section: DynamicFormSection<TValues>;
register: UseFormRegister<TValues>;
control: Control<TValues>;
errors: FieldErrors<TValues>;
footer?: ReactNode;
}
const DynamicSectionComponent = <TValues extends FieldValues>({
section,
register,
control,
errors,
footer,
}: DynamicSectionProps<TValues>) => (
<ShowcaseSection
title={section.title}
rootClassName={section.rootClassName}
className={cn("grid grid-cols-1 gap-3 md:grid-cols-2", section.className)}
>
{section.fields.map((field) => {
if (field.hidden) return null;
if (field.kind === "custom") {
return (
<div key={field.name} className={field.className}>
{field.render({
control,
errors,
register,
error: getErrorMessage(errors, field.name),
})}
</div>
);
}
return (
<InputGroup<TValues>
key={field.name}
{...register(field.name, field.rules)}
name={field.name}
label={field.label}
type={field.type}
placeholder={field.placeholder}
required={field.required}
disabled={field.disabled}
autoComplete={field.autoComplete}
dataCy={field.dataCy}
icon={field.icon}
iconPosition={field.iconPosition}
className={field.className}
errors={errors}
/>
);
})}
{footer ? <div className="md:col-span-2">{footer}</div> : null}
</ShowcaseSection>
);
const DynamicSection = memo(
DynamicSectionComponent,
) as typeof DynamicSectionComponent;
const DynamicFormBuilder = <TValues extends FieldValues>({
sections,
register,
@@ -113,57 +179,19 @@ const DynamicFormBuilder = <TValues extends FieldValues>({
{header}
<div className={sectionsClassName}>
{sections.map((section, sectionIndex) => (
<ShowcaseSection
key={sectionIndex}
title={section.title}
rootClassName={section.rootClassName}
className={cn(
"grid grid-cols-1 gap-3 md:grid-cols-2",
section.className,
)}
>
{section.fields.map((field) => {
if (field.hidden) return null;
if (field.kind === "custom") {
return (
<div key={field.name} className={field.className}>
{field.render({
control,
errors,
register,
error: getErrorMessage(errors, field.name),
})}
</div>
);
}
return (
<InputGroup<TValues>
key={field.name}
{...register(field.name, field.rules)}
name={field.name}
label={field.label}
type={field.type}
placeholder={field.placeholder}
required={field.required}
disabled={field.disabled}
autoComplete={field.autoComplete}
dataCy={field.dataCy}
icon={field.icon}
iconPosition={field.iconPosition}
className={field.className}
errors={errors}
/>
);
})}
{footer &&
footerPlacement === "last-section" &&
sectionIndex === sections.length - 1 ? (
<div className="md:col-span-2">{footer}</div>
) : null}
</ShowcaseSection>
<DynamicSection
key={section.id}
section={section}
register={register}
control={control}
errors={errors}
footer={
footerPlacement === "last-section" &&
sectionIndex === sections.length - 1
? footer
: undefined
}
/>
))}
</div>
{footerPlacement === "form" ? footer : null}
@@ -8,8 +8,11 @@ import FileImageUploadCard from "@/components/ui/FileImageUploadCard";
import { FormErrorMessages } from "@/constants/Texts";
import type { Dispatch, SetStateAction } from "react";
import { Controller, type UseFormRegisterReturn } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import type { CreateAdminFormValues } from "./createAdminForm.types";
const adminSectionId = uuidv4();
interface CreateAdminFormSectionsOptions {
editMode?: boolean;
passwordFieldInputType: "password" | "text";
@@ -36,6 +39,7 @@ export const createAdminFormSections = ({
clearSelectedProfileImage,
}: CreateAdminFormSectionsOptions): DynamicFormSection<CreateAdminFormValues>[] => [
{
id: adminSectionId,
title: "Input Fields",
fields: [
{
@@ -2,11 +2,15 @@ import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import type { DynamicFormSection } from "@/components/forms/DynamicFormBuilder";
import { FormErrorMessages } from "@/constants/Texts";
import type { TCreateCompanyCategoryCredentials } from "@/lib/api";
import { v4 as uuidv4 } from "uuid";
const companyCategorySectionId = uuidv4();
export const createCompanyCategoryFormSections = (
editMode?: boolean,
): DynamicFormSection<TCreateCompanyCategoryCredentials>[] => [
{
id: companyCategorySectionId,
title: editMode ? "Edit Company Category" : "Create Company Category",
className: "gap-5",
fields: [
@@ -9,8 +9,16 @@ import { FormErrorMessages } from "@/constants/Texts";
import type { ICreateCompanyCredentials } from "@/lib/api";
import type { ILabelValue } from "@/types/shared";
import type { UseFormSetValue, UseFormWatch } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
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 {
editMode?: boolean;
id?: string;
@@ -79,6 +87,7 @@ export const createCompanyFormSections = ({
setValue,
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
{
id: companyInformationSectionId,
title: "Company information",
rootClassName: "lg:col-span-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",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [
@@ -276,6 +286,7 @@ export const createCompanyFormSections = ({
],
},
{
id: settingsSectionId,
title: "Settings",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [
@@ -323,6 +334,7 @@ export const createCompanyFormSections = ({
],
},
{
id: addressSectionId,
title: "Address",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [
@@ -395,6 +407,7 @@ export const createCompanyFormSections = ({
],
},
{
id: tagsSectionId,
title: "Tags",
className: "gap-x-5 gap-y-5 sm:grid-cols-2 md:grid-cols-2",
fields: [
@@ -479,6 +492,7 @@ export const createCompanyFormSections = ({
],
},
{
id: documentsSectionId,
title: "Documents",
rootClassName: "lg:col-span-2",
className: "md:grid-cols-1",
@@ -14,6 +14,9 @@ import type { ILabelValue } from "@/types/shared";
import { getEnumAsArray } from "@/utils/shared";
import type { Dispatch, SetStateAction } from "react";
import { Controller, type UseFormWatch } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
const customerSectionId = uuidv4();
interface CustomerFormSectionsOptions {
editMode?: boolean;
@@ -31,6 +34,7 @@ export const createCustomerFormSections = ({
setShowPassword,
}: CustomerFormSectionsOptions): DynamicFormSection<CreateCustomerCredentials>[] => [
{
id: customerSectionId,
title: "Create Customer",
className: "gap-5",
fields: [
@@ -8,6 +8,9 @@ import { FormErrorMessages } from "@/constants/Texts";
import type { TCreateNotificationCredentials } from "@/lib/api/types/NotificationHistory";
import type { ILabelValue } from "@/types/shared";
import type { ChangeEvent } from "react";
import { v4 as uuidv4 } from "uuid";
const notificationSectionId = uuidv4();
interface NotificationFormSectionsOptions {
recipient: ILabelValue[] | null;
@@ -37,6 +40,7 @@ export const createNotificationFormSections = ({
channels,
}: NotificationFormSectionsOptions): DynamicFormSection<TCreateNotificationCredentials>[] => [
{
id: notificationSectionId,
title: "Create Notification",
className: "gap-5",
fields: [
+3 -2
View File
@@ -4,8 +4,9 @@ export function Logo() {
return (
<Image
src="/images/main-logo.svg"
width={174}
height={32}
width={1217}
height={192}
className="h-auto w-[174px]"
alt="Opp lens logo"
role="presentation"
quality={100}