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.
This commit is contained in:
AmirReza Jamali
2026-04-19 16:14:51 +03:30
parent 69e75eb9b8
commit 57e9d90eb1
9 changed files with 114 additions and 70 deletions
+4 -4
View File
@@ -9,8 +9,8 @@ const Breadcrumb = ({ items }: BreadcrumbProps) => {
const currentPage = items[items.length - 1]; const currentPage = items[items.length - 1];
return ( return (
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-[26px] font-bold leading-[30px] text-dark dark:text-white"> <h2 className="text-[16px] font-bold leading-[30px] text-dark dark:text-white">
{currentPage.name} {currentPage.name}
</h2> </h2>
@@ -20,14 +20,14 @@ const Breadcrumb = ({ items }: BreadcrumbProps) => {
const isLast = index === items.length - 1; const isLast = index === items.length - 1;
return isLast ? ( return isLast ? (
<li key={index} className="font-medium text-primary"> <li key={index} className="font-medium text-sm text-primary">
{item.name} {item.name}
</li> </li>
) : ( ) : (
<li key={index} className="flex items-center gap-2"> <li key={index} className="flex items-center gap-2">
<Link <Link
href={item.href} href={item.href}
className="font-medium hover:text-primary" className="font-medium text-sm hover:text-primary"
> >
{item.name} {item.name}
</Link> </Link>
@@ -16,7 +16,7 @@ type InputGroupProps<T extends FieldValues> = {
name: Path<T>; name: Path<T>;
label: string; label: string;
type: HTMLInputTypeAttribute; type: HTMLInputTypeAttribute;
placeholder: string; placeholder?: string;
className?: string; className?: string;
fileStyleVariant?: "style1" | "style2"; fileStyleVariant?: "style1" | "style2";
required?: boolean; required?: boolean;
@@ -93,7 +93,7 @@ const InputGroup = <T extends FieldValues>({
defaultValue: props.defaultValue, defaultValue: props.defaultValue,
})} })}
className={cn( 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 // Add error styles conditionally
error && "border-red focus:border-red dark:border-red", error && "border-red focus:border-red dark:border-red",
type === "file" type === "file"
@@ -18,7 +18,7 @@ function MainLayout({ children }: PropsWithChildren) {
{isMobile && <div className="w-[70px] shrink-0" />} {isMobile && <div className="w-[70px] shrink-0" />}
<div className="flex min-h-0 min-w-0 flex-1 flex-col bg-gray-2 dark:bg-[#020d1a]"> <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-gray-2 dark:bg-[#020d1a]">
<Header /> <Header />
<main className="isolate flex min-h-0 flex-1 flex-col mx-auto w-full max-w-screen-2xl overflow-hidden p-4 md:p-6 2xl:p-10"> <main className="isolate flex min-h-0 flex-1 flex-col mx-auto w-full max-w-screen-2xl overflow-hidden p-4 md:p-6 2xl:px-10 2xl:py-4">
{children} {children}
</main> </main>
</div> </div>
+2 -3
View File
@@ -6,12 +6,11 @@ import { UserInfo } from "./user-info";
export function Header() { export function Header() {
return ( return (
<header className="sticky top-0 z-30 flex shrink-0 items-center justify-between border-b border-stroke bg-white px-4 py-5 shadow-1 dark:border-stroke-dark dark:bg-gray-dark md:px-5 2xl:px-10"> <header className="sticky top-0 z-30 flex shrink-0 items-center justify-between border-b border-stroke bg-white px-4 py-2.5 shadow-1 dark:border-stroke-dark dark:bg-gray-dark md:px-5 2xl:px-10">
<div className="max-xl:hidden"> <div className="max-xl:hidden">
<h1 className="mb-0.5 text-heading-5 font-bold text-dark dark:text-white"> <h1 className="text-heading-6 font-bold leading-tight text-dark dark:text-white">
Opportunity Lens Opportunity Lens
</h1> </h1>
<p className="font-medium">Opportunity Lens management panel</p>
</div> </div>
<div className="flex flex-1 items-center justify-end gap-2 min-[375px]:gap-4"> <div className="flex flex-1 items-center justify-end gap-2 min-[375px]:gap-4">
+8 -2
View File
@@ -4,12 +4,18 @@ import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import ListHeader from "@/components/ui/ListHeader"; import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal"; 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 TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip"; import { _TooltipDefaultParams } from "@/constants/tooltip";
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared"; import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { TableHeader, TableRow } from "../../ui/table";
import CmsListFilters from "./CmsListFilters"; import CmsListFilters from "./CmsListFilters";
import useCmsTablePresenter from "./useCmsTablePresenter"; import useCmsTablePresenter from "./useCmsTablePresenter";
@@ -3,7 +3,8 @@ import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import { Switch } from "@/components/FormElements/switch"; import { Switch } from "@/components/FormElements/switch";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper"; import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; 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 { import {
Table, Table,
TableBody, TableBody,
@@ -15,21 +16,19 @@ import {
import TableSkeleton from "@/components/ui/TableSkeleton"; import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip"; import { _TooltipDefaultParams } from "@/constants/tooltip";
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared"; import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
import { startTransition } from "react";
import { Tooltip } from "react-tooltip"; 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 CompanyCategoryListFilters from "./CompanyCategoryListFilters";
import Modal from "@/components/ui/modal"; import useCompanyCategoriesPresenter from "./useCompanyCategoriesPresenter";
const CompanyCategoriesTable = () => { const CompanyCategoriesTable = () => {
const { const {
categories, optimisticCategories,
addOptimisticPublished,
isLoading, isLoading,
pathName, pathName,
router, router,
currentCategory, currentCategory,
isToggling,
togglePublished, togglePublished,
onConfirmDelete, onConfirmDelete,
setCurrentCategory, setCurrentCategory,
@@ -42,18 +41,10 @@ const CompanyCategoriesTable = () => {
search, search,
watch, watch,
setFilterValue, setFilterValue,
columns,
isMutating, isMutating,
} = useCompanyCategoriesPresenter(); } = useCompanyCategoriesPresenter();
const columns = [
"Row",
"Title",
"Description",
"Created at",
"Published",
"Actions",
];
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<ListHeader <ListHeader
@@ -74,11 +65,11 @@ const CompanyCategoriesTable = () => {
{isLoading && <TableSkeleton column={columns.length} />} {isLoading && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
<EmptyListWrapper <EmptyListWrapper
list={categories ?? []} list={optimisticCategories}
columns={columns} columns={columns}
emptyMessage="No categories were found" emptyMessage="No categories were found"
> >
{(categories ?? []).map((category, index) => ( {optimisticCategories.map((category, index) => (
<TableRow <TableRow
key={category.id} key={category.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -92,21 +83,21 @@ const CompanyCategoriesTable = () => {
{unixToDate({ unix: category.created_at, hasTime: true })} {unixToDate({ unix: category.created_at, hasTime: true })}
</TableCell> </TableCell>
<TableCell colSpan={100}> <TableCell colSpan={100}>
<IsVisible condition={isToggling}> <Switch
<Skeleton className="h-8 bg-neutral-400 dark:bg-dark-4" /> withIcon
</IsVisible> onToggle={() => {}}
<IsVisible condition={!isToggling}> onChange={() => {
<Switch const nextPublished = !category.published;
withIcon startTransition(async () => {
onChange={(e) => { addOptimisticPublished({
togglePublished(category.id); id: category.id,
}} published: nextPublished,
checked={category.published} });
onToggle={(e) => { await togglePublished(category.id);
console.log(e); });
}} }}
/> checked={category.published}
</IsVisible> />
</TableCell> </TableCell>
<TableCell className="text-start xl:pr-7.5"> <TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5"> <div className="flex items-center justify-start gap-x-3.5">
@@ -9,9 +9,11 @@ import { TCompanyCategory } from "@/lib/api";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useOptimistic, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
type TogglePublishedOptimistic = Pick<TCompanyCategory, "id" | "published">;
const useCompanyCategoriesPresenter = () => { const useCompanyCategoriesPresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
@@ -42,10 +44,17 @@ const useCompanyCategoriesPresenter = () => {
}, [searchParams, filterFormReset]); }, [searchParams, filterFormReset]);
const { data, isLoading, isError } = useCompanyCategoriesQuery(params); 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(() => const { mutate: deleteCategory } = useDeleteCompanyCategoryQuery(() =>
setIsModalOpen(false), setIsModalOpen(false),
); );
const { mutate: togglePublished, isPending: isToggling } = const { mutateAsync: togglePublished } =
useToggleCompanyCategoryPublished(); useToggleCompanyCategoryPublished();
const search = (data: any) => { const search = (data: any) => {
@@ -60,19 +69,29 @@ const useCompanyCategoriesPresenter = () => {
if (!currentCategory) return; if (!currentCategory) return;
deleteCategory(currentCategory?.id); deleteCategory(currentCategory?.id);
}; };
const columns = [
"Row",
"Title",
"Description",
"Created at",
"Published",
"Actions",
];
return { return {
categories: data?.data.categories, categories,
optimisticCategories,
addOptimisticPublished,
isModalOpen, isModalOpen,
setIsModalOpen, setIsModalOpen,
isLoading, isLoading,
isError, isError,
pathName, pathName,
router, router,
columns,
params, params,
setParams, setParams,
currentCategory, currentCategory,
togglePublished, togglePublished,
isToggling,
setCurrentCategory, setCurrentCategory,
onConfirmDelete, onConfirmDelete,
isFilterModalOpen, isFilterModalOpen,
+13 -18
View File
@@ -13,12 +13,11 @@ interface IProps {
id?: string; id?: string;
} }
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => { const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
const { errors, handleSubmit, register, onSubmit } = const { errors, handleSubmit, register, onSubmit } = useCreateAdminPresenter({
useCreateAdminPresenter({ editMode,
editMode, defaultValues,
defaultValues, id,
id, });
});
return ( return (
<form <form
@@ -30,7 +29,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div className="flex flex-col gap-9"> <div className="flex flex-col gap-9">
<ShowcaseSection <ShowcaseSection
title="Input Fields" title="Input Fields"
className="grid grid-cols-1 gap-5 md:grid-cols-2" className="grid grid-cols-1 gap-3 md:grid-cols-2"
> >
<div> <div>
<InputGroup <InputGroup
@@ -46,7 +45,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
}, },
})} })}
label="Full name" label="Full name"
placeholder="Full name" placeholder="Enter full name (5-100 characters)"
required required
type="text" type="text"
name="full_name" name="full_name"
@@ -61,7 +60,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div> <div>
<InputGroup <InputGroup
label="Username" label="Username"
placeholder="Username" placeholder="Letters and numbers only (3-30)"
autoComplete="off" autoComplete="off"
required required
type="text" type="text"
@@ -96,7 +95,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<InputGroup <InputGroup
label="Email" label="Email"
autoComplete="off" autoComplete="off"
placeholder="Email" placeholder="you@example.com"
type="email" type="email"
required required
{...register("email", { {...register("email", {
@@ -121,8 +120,8 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div> <div>
<InputGroup <InputGroup
label="Password" label="Password"
placeholder="Password"
autoComplete="new-password" autoComplete="new-password"
placeholder="At least 8 characters"
required required
type="password" type="password"
{...register("password", { {...register("password", {
@@ -152,7 +151,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div> <div>
<InputGroup <InputGroup
label="Phone" label="Phone"
placeholder="Phone"
type="text" type="text"
{...register("phone", { {...register("phone", {
minLength: { minLength: {
@@ -163,9 +161,9 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
value: 20, value: 20,
message: FormErrorMessages.maxLength(20), message: FormErrorMessages.maxLength(20),
}, },
pattern:{ pattern: {
value:REGEX.PHONE, value: REGEX.PHONE,
message:FormErrorMessages.invalidPattern message: FormErrorMessages.invalidPattern,
}, },
})} })}
name="phone" name="phone"
@@ -180,7 +178,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div> <div>
<InputGroup <InputGroup
label="Department" label="Department"
placeholder="Department"
type="text" type="text"
{...register("department", { {...register("department", {
minLength: { minLength: {
@@ -204,7 +201,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div> <div>
<InputGroup <InputGroup
label="Position" label="Position"
placeholder="Position"
type="text" type="text"
{...register("position", { {...register("position", {
minLength: { minLength: {
@@ -230,7 +226,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
type="file" type="file"
fileStyleVariant="style1" fileStyleVariant="style1"
label="Attach file" label="Attach file"
placeholder="Attach file"
register={register} register={register}
name="profile_image" name="profile_image"
/> />
+37 -3
View File
@@ -2,6 +2,7 @@ import {
API_ENDPOINTS, API_ENDPOINTS,
companiesService, companiesService,
companyCategoriesService, companyCategoriesService,
type TCompanyCategoryApiResponse,
} from "@/lib/api"; } from "@/lib/api";
import { import {
useInfiniteQuery, useInfiniteQuery,
@@ -10,7 +11,7 @@ import {
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo } from "react"; import { useCallback, useMemo } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
export const useCreateCompany = () => { export const useCreateCompany = () => {
@@ -171,6 +172,30 @@ export const useDeleteCompanyCategoryQuery = (successCallback?: () => void) => {
}; };
export const useToggleCompanyCategoryPublished = () => { export const useToggleCompanyCategoryPublished = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const updateOptimisticCategories = useCallback((categoryId: string) => {
queryClient.setQueriesData<TCompanyCategoryApiResponse>(
{
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( const mutationKey = useMemo(
() => [ () => [
API_ENDPOINTS.COMPANIES.CATEGORIES.TOGGLE_PUBLISHED("published"), API_ENDPOINTS.COMPANIES.CATEGORIES.TOGGLE_PUBLISHED("published"),
@@ -181,8 +206,17 @@ export const useToggleCompanyCategoryPublished = () => {
return useMutation({ return useMutation({
mutationKey, mutationKey,
mutationFn: companyCategoriesService.togglePublished, mutationFn: companyCategoriesService.togglePublished,
onSuccess: ({ data }: { data: { message: string } }) => { onSuccess: (toggleResponse, categoryId: string) => {
toast.success(data.message); 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({ queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.COMPANIES.CATEGORIES.READ_ALL], queryKey: [API_ENDPOINTS.COMPANIES.CATEGORIES.READ_ALL],
}); });