From 57e9d90eb114c1feb7a850feec002e7ae069c24a Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 19 Apr 2026 16:14:51 +0330 Subject: [PATCH] refactor(Breadcrumbs, InputGroup, Layouts, Tables): enhance styling and structure for improved responsiveness - Adjusted Breadcrumb component styles for better spacing and font size. - Made placeholder in InputGroup optional and refined input styles for consistency. - Updated MainLayout and Header components for improved padding and layout. - Enhanced table components by importing necessary elements and optimizing rendering logic. - Refactored CompanyCategoriesTable to utilize optimistic updates for better user experience. --- src/components/Breadcrumbs/Breadcrumb.tsx | 8 +-- .../FormElements/InputGroup/index.tsx | 4 +- src/components/Layouts/conditional-layout.tsx | 2 +- src/components/Layouts/header/index.tsx | 5 +- src/components/Tables/cms/index.tsx | 10 +++- .../companies/company-categories/index.tsx | 57 ++++++++----------- .../useCompanyCategoriesPresenter.ts | 27 +++++++-- src/components/forms/admins/CreateAdmin.tsx | 31 +++++----- src/hooks/queries/useCompaniesQueries.ts | 40 ++++++++++++- 9 files changed, 114 insertions(+), 70 deletions(-) diff --git a/src/components/Breadcrumbs/Breadcrumb.tsx b/src/components/Breadcrumbs/Breadcrumb.tsx index f49bbf3..493d7ac 100644 --- a/src/components/Breadcrumbs/Breadcrumb.tsx +++ b/src/components/Breadcrumbs/Breadcrumb.tsx @@ -9,8 +9,8 @@ const Breadcrumb = ({ items }: BreadcrumbProps) => { const currentPage = items[items.length - 1]; return ( -
-

+
+

{currentPage.name}

@@ -20,14 +20,14 @@ const Breadcrumb = ({ items }: BreadcrumbProps) => { const isLast = index === items.length - 1; return isLast ? ( -
  • +
  • {item.name}
  • ) : (
  • {item.name} diff --git a/src/components/FormElements/InputGroup/index.tsx b/src/components/FormElements/InputGroup/index.tsx index 9ed8923..fc3dc7d 100644 --- a/src/components/FormElements/InputGroup/index.tsx +++ b/src/components/FormElements/InputGroup/index.tsx @@ -16,7 +16,7 @@ type InputGroupProps = { name: Path; label: string; type: HTMLInputTypeAttribute; - placeholder: string; + placeholder?: string; className?: string; fileStyleVariant?: "style1" | "style2"; required?: boolean; @@ -93,7 +93,7 @@ const InputGroup = ({ defaultValue: props.defaultValue, })} className={cn( - "w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition placeholder:capitalize focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary", + "w-full rounded-lg border-[1.5px] border-stroke bg-transparent outline-none transition focus:border-primary disabled:cursor-default disabled:bg-gray-2 data-[active=true]:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary", // Add error styles conditionally error && "border-red focus:border-red dark:border-red", type === "file" diff --git a/src/components/Layouts/conditional-layout.tsx b/src/components/Layouts/conditional-layout.tsx index f88fb5c..3c73f2c 100644 --- a/src/components/Layouts/conditional-layout.tsx +++ b/src/components/Layouts/conditional-layout.tsx @@ -18,7 +18,7 @@ function MainLayout({ children }: PropsWithChildren) { {isMobile &&
    }
    -
    +
    {children}
    diff --git a/src/components/Layouts/header/index.tsx b/src/components/Layouts/header/index.tsx index af93a3c..dd69d0f 100644 --- a/src/components/Layouts/header/index.tsx +++ b/src/components/Layouts/header/index.tsx @@ -6,12 +6,11 @@ import { UserInfo } from "./user-info"; export function Header() { return ( -
    +
    -

    +

    Opportunity Lens

    -

    Opportunity Lens management panel

    diff --git a/src/components/Tables/cms/index.tsx b/src/components/Tables/cms/index.tsx index 0bce796..176d819 100644 --- a/src/components/Tables/cms/index.tsx +++ b/src/components/Tables/cms/index.tsx @@ -4,12 +4,18 @@ import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ListHeader from "@/components/ui/ListHeader"; import Modal from "@/components/ui/modal"; -import { Table, TableBody, TableCell, TableHead } from "@/components/ui/table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import TableSkeleton from "@/components/ui/TableSkeleton"; import { _TooltipDefaultParams } from "@/constants/tooltip"; import { getPaginatedRowNumber, unixToDate } from "@/utils/shared"; import { Tooltip } from "react-tooltip"; -import { TableHeader, TableRow } from "../../ui/table"; import CmsListFilters from "./CmsListFilters"; import useCmsTablePresenter from "./useCmsTablePresenter"; diff --git a/src/components/Tables/companies/company-categories/index.tsx b/src/components/Tables/companies/company-categories/index.tsx index e0f9da4..0100f8e 100644 --- a/src/components/Tables/companies/company-categories/index.tsx +++ b/src/components/Tables/companies/company-categories/index.tsx @@ -3,7 +3,8 @@ import { PencilSquareIcon, TrashIcon } from "@/assets/icons"; import { Switch } from "@/components/FormElements/switch"; import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; import ConfirmationModal from "@/components/ui/ConfirmationModal"; -import IsVisible from "@/components/ui/IsVisible"; +import ListHeader from "@/components/ui/ListHeader"; +import Modal from "@/components/ui/modal"; import { Table, TableBody, @@ -15,21 +16,19 @@ import { import TableSkeleton from "@/components/ui/TableSkeleton"; import { _TooltipDefaultParams } from "@/constants/tooltip"; import { getPaginatedRowNumber, unixToDate } from "@/utils/shared"; +import { startTransition } from "react"; import { Tooltip } from "react-tooltip"; -import { Skeleton } from "../../../ui/skeleton"; -import useCompanyCategoriesPresenter from "./useCompanyCategoriesPresenter"; -import ListHeader from "@/components/ui/ListHeader"; import CompanyCategoryListFilters from "./CompanyCategoryListFilters"; -import Modal from "@/components/ui/modal"; +import useCompanyCategoriesPresenter from "./useCompanyCategoriesPresenter"; const CompanyCategoriesTable = () => { const { - categories, + optimisticCategories, + addOptimisticPublished, isLoading, pathName, router, currentCategory, - isToggling, togglePublished, onConfirmDelete, setCurrentCategory, @@ -42,18 +41,10 @@ const CompanyCategoriesTable = () => { search, watch, setFilterValue, + columns, isMutating, } = useCompanyCategoriesPresenter(); - const columns = [ - "Row", - "Title", - "Description", - "Created at", - "Published", - "Actions", - ]; - return (
    { {isLoading && } - {(categories ?? []).map((category, index) => ( + {optimisticCategories.map((category, index) => ( { {unixToDate({ unix: category.created_at, hasTime: true })} - - - - - { - togglePublished(category.id); - }} - checked={category.published} - onToggle={(e) => { - console.log(e); - }} - /> - + {}} + onChange={() => { + const nextPublished = !category.published; + startTransition(async () => { + addOptimisticPublished({ + id: category.id, + published: nextPublished, + }); + await togglePublished(category.id); + }); + }} + checked={category.published} + />
    diff --git a/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts b/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts index 9097ce3..20046c3 100644 --- a/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts +++ b/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts @@ -9,9 +9,11 @@ import { TCompanyCategory } from "@/lib/api"; import { deleteEmptyKeys } from "@/utils/shared"; import { useIsMutating } from "@tanstack/react-query"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useOptimistic, useState } from "react"; import { useForm } from "react-hook-form"; +type TogglePublishedOptimistic = Pick; + const useCompanyCategoriesPresenter = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); @@ -42,10 +44,17 @@ const useCompanyCategoriesPresenter = () => { }, [searchParams, filterFormReset]); const { data, isLoading, isError } = useCompanyCategoriesQuery(params); + const categories = data?.data.categories; + const [optimisticCategories, addOptimisticPublished] = useOptimistic< + TCompanyCategory[], + TogglePublishedOptimistic + >(categories ?? [], (state, { id, published }) => + state.map((c) => (c.id === id ? { ...c, published } : c)), + ); const { mutate: deleteCategory } = useDeleteCompanyCategoryQuery(() => setIsModalOpen(false), ); - const { mutate: togglePublished, isPending: isToggling } = + const { mutateAsync: togglePublished } = useToggleCompanyCategoryPublished(); const search = (data: any) => { @@ -60,19 +69,29 @@ const useCompanyCategoriesPresenter = () => { if (!currentCategory) return; deleteCategory(currentCategory?.id); }; + const columns = [ + "Row", + "Title", + "Description", + "Created at", + "Published", + "Actions", + ]; return { - categories: data?.data.categories, + categories, + optimisticCategories, + addOptimisticPublished, isModalOpen, setIsModalOpen, isLoading, isError, pathName, router, + columns, params, setParams, currentCategory, togglePublished, - isToggling, setCurrentCategory, onConfirmDelete, isFilterModalOpen, diff --git a/src/components/forms/admins/CreateAdmin.tsx b/src/components/forms/admins/CreateAdmin.tsx index c17326e..09518b5 100644 --- a/src/components/forms/admins/CreateAdmin.tsx +++ b/src/components/forms/admins/CreateAdmin.tsx @@ -13,12 +13,11 @@ interface IProps { id?: string; } const CreateAdminForm: FC = ({ defaultValues, editMode, id }) => { - const { errors, handleSubmit, register, onSubmit } = - useCreateAdminPresenter({ - editMode, - defaultValues, - id, - }); + const { errors, handleSubmit, register, onSubmit } = useCreateAdminPresenter({ + editMode, + defaultValues, + id, + }); return (
    = ({ defaultValues, editMode, id }) => {
    = ({ defaultValues, editMode, id }) => { }, })} label="Full name" - placeholder="Full name" + placeholder="Enter full name (5-100 characters)" required type="text" name="full_name" @@ -61,7 +60,7 @@ const CreateAdminForm: FC = ({ defaultValues, editMode, id }) => {
    = ({ defaultValues, editMode, id }) => { = ({ defaultValues, editMode, id }) => {
    = ({ defaultValues, editMode, id }) => {
    = ({ defaultValues, editMode, id }) => { value: 20, message: FormErrorMessages.maxLength(20), }, - pattern:{ - value:REGEX.PHONE, - message:FormErrorMessages.invalidPattern + pattern: { + value: REGEX.PHONE, + message: FormErrorMessages.invalidPattern, }, })} name="phone" @@ -180,7 +178,6 @@ const CreateAdminForm: FC = ({ defaultValues, editMode, id }) => {
    = ({ defaultValues, editMode, id }) => {
    = ({ defaultValues, editMode, id }) => { type="file" fileStyleVariant="style1" label="Attach file" - placeholder="Attach file" register={register} name="profile_image" /> diff --git a/src/hooks/queries/useCompaniesQueries.ts b/src/hooks/queries/useCompaniesQueries.ts index fa74ead..9c8e9ae 100644 --- a/src/hooks/queries/useCompaniesQueries.ts +++ b/src/hooks/queries/useCompaniesQueries.ts @@ -2,6 +2,7 @@ import { API_ENDPOINTS, companiesService, companyCategoriesService, + type TCompanyCategoryApiResponse, } from "@/lib/api"; import { useInfiniteQuery, @@ -10,7 +11,7 @@ import { useQueryClient, } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { toast } from "react-toastify"; export const useCreateCompany = () => { @@ -171,6 +172,30 @@ export const useDeleteCompanyCategoryQuery = (successCallback?: () => void) => { }; export const useToggleCompanyCategoryPublished = () => { const queryClient = useQueryClient(); + const updateOptimisticCategories = useCallback((categoryId: string) => { + queryClient.setQueriesData( + { + queryKey: [API_ENDPOINTS.COMPANIES.CATEGORIES.READ_ALL], + exact: false, + }, + (previousList) => { + if (!previousList?.data?.categories) { + return previousList; + } + return { + ...previousList, + data: { + ...previousList.data, + categories: previousList.data.categories.map((category) => + category.id === categoryId + ? { ...category, published: !category.published } + : category, + ), + }, + }; + }, + ); + }, [queryClient]); const mutationKey = useMemo( () => [ API_ENDPOINTS.COMPANIES.CATEGORIES.TOGGLE_PUBLISHED("published"), @@ -181,8 +206,17 @@ export const useToggleCompanyCategoryPublished = () => { return useMutation({ mutationKey, mutationFn: companyCategoriesService.togglePublished, - onSuccess: ({ data }: { data: { message: string } }) => { - toast.success(data.message); + onSuccess: (toggleResponse, categoryId: string) => { + const responseBody = toggleResponse as { + data?: { message?: string }; + message?: string; + }; + const toastMessage = + responseBody.data?.message ?? responseBody.message; + if (toastMessage) { + toast.success(toastMessage); + } + updateOptimisticCategories(categoryId); queryClient.invalidateQueries({ queryKey: [API_ENDPOINTS.COMPANIES.CATEGORIES.READ_ALL], });