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];
return (
<div className="mb-6 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">
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-[16px] font-bold leading-[30px] text-dark dark:text-white">
{currentPage.name}
</h2>
@@ -20,14 +20,14 @@ const Breadcrumb = ({ items }: BreadcrumbProps) => {
const isLast = index === items.length - 1;
return isLast ? (
<li key={index} className="font-medium text-primary">
<li key={index} className="font-medium text-sm text-primary">
{item.name}
</li>
) : (
<li key={index} className="flex items-center gap-2">
<Link
href={item.href}
className="font-medium hover:text-primary"
className="font-medium text-sm hover:text-primary"
>
{item.name}
</Link>
@@ -16,7 +16,7 @@ type InputGroupProps<T extends FieldValues> = {
name: Path<T>;
label: string;
type: HTMLInputTypeAttribute;
placeholder: string;
placeholder?: string;
className?: string;
fileStyleVariant?: "style1" | "style2";
required?: boolean;
@@ -93,7 +93,7 @@ const InputGroup = <T extends FieldValues>({
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"
@@ -18,7 +18,7 @@ function MainLayout({ children }: PropsWithChildren) {
{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]">
<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}
</main>
</div>
+2 -3
View File
@@ -6,12 +6,11 @@ import { UserInfo } from "./user-info";
export function Header() {
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">
<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
</h1>
<p className="font-medium">Opportunity Lens management panel</p>
</div>
<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 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";
@@ -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 (
<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
@@ -74,11 +65,11 @@ const CompanyCategoriesTable = () => {
{isLoading && <TableSkeleton column={columns.length} />}
<TableBody>
<EmptyListWrapper
list={categories ?? []}
list={optimisticCategories}
columns={columns}
emptyMessage="No categories were found"
>
{(categories ?? []).map((category, index) => (
{optimisticCategories.map((category, index) => (
<TableRow
key={category.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -92,21 +83,21 @@ const CompanyCategoriesTable = () => {
{unixToDate({ unix: category.created_at, hasTime: true })}
</TableCell>
<TableCell colSpan={100}>
<IsVisible condition={isToggling}>
<Skeleton className="h-8 bg-neutral-400 dark:bg-dark-4" />
</IsVisible>
<IsVisible condition={!isToggling}>
<Switch
withIcon
onChange={(e) => {
togglePublished(category.id);
}}
checked={category.published}
onToggle={(e) => {
console.log(e);
}}
/>
</IsVisible>
<Switch
withIcon
onToggle={() => {}}
onChange={() => {
const nextPublished = !category.published;
startTransition(async () => {
addOptimisticPublished({
id: category.id,
published: nextPublished,
});
await togglePublished(category.id);
});
}}
checked={category.published}
/>
</TableCell>
<TableCell className="text-start xl:pr-7.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 { 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<TCompanyCategory, "id" | "published">;
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,
+13 -18
View File
@@ -13,12 +13,11 @@ interface IProps {
id?: string;
}
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
const { errors, handleSubmit, register, onSubmit } =
useCreateAdminPresenter({
editMode,
defaultValues,
id,
});
const { errors, handleSubmit, register, onSubmit } = useCreateAdminPresenter({
editMode,
defaultValues,
id,
});
return (
<form
@@ -30,7 +29,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div className="flex flex-col gap-9">
<ShowcaseSection
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>
<InputGroup
@@ -46,7 +45,7 @@ const CreateAdminForm: FC<IProps> = ({ 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<IProps> = ({ defaultValues, editMode, id }) => {
<div>
<InputGroup
label="Username"
placeholder="Username"
placeholder="Letters and numbers only (3-30)"
autoComplete="off"
required
type="text"
@@ -96,7 +95,7 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<InputGroup
label="Email"
autoComplete="off"
placeholder="Email"
placeholder="you@example.com"
type="email"
required
{...register("email", {
@@ -121,8 +120,8 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div>
<InputGroup
label="Password"
placeholder="Password"
autoComplete="new-password"
placeholder="At least 8 characters"
required
type="password"
{...register("password", {
@@ -152,7 +151,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div>
<InputGroup
label="Phone"
placeholder="Phone"
type="text"
{...register("phone", {
minLength: {
@@ -163,9 +161,9 @@ const CreateAdminForm: FC<IProps> = ({ 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<IProps> = ({ defaultValues, editMode, id }) => {
<div>
<InputGroup
label="Department"
placeholder="Department"
type="text"
{...register("department", {
minLength: {
@@ -204,7 +201,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
<div>
<InputGroup
label="Position"
placeholder="Position"
type="text"
{...register("position", {
minLength: {
@@ -230,7 +226,6 @@ const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
type="file"
fileStyleVariant="style1"
label="Attach file"
placeholder="Attach file"
register={register}
name="profile_image"
/>
+37 -3
View File
@@ -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<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(
() => [
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],
});