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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
+23
-4
@@ -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,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"
|
||||
/>
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user