feat(dependencies): add Lottie animation libraries and refactor NotFoundPage

- Added `@lottiefiles/dotlottie-react` and `lottie-react` dependencies for enhanced animation capabilities.
- Refactored the NotFoundPage component to utilize a dedicated NotFoundContent component, improving code organization and readability.
- Updated package.json and pnpm-lock.yaml to reflect new dependencies and their versions.
This commit is contained in:
AmirReza Jamali
2026-05-13 17:07:47 +03:30
parent d1bc56dc60
commit 25f8bde229
49 changed files with 3511 additions and 2197 deletions
@@ -1,4 +1,8 @@
"use client";
import { Select } from "@/components/FormElements/select";
import { Button } from "@/components/ui-elements/button";
import InlineListFiltersPanel from "@/components/ui/InlineListFiltersPanel";
import { AdminRoles, CustomerStatus, CustomerType } from "@/constants/enums";
import { getEnumAsArray } from "@/utils/shared";
import {
@@ -8,82 +12,113 @@ import {
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
import { useMemo } from "react";
interface CustomerListFiltersProps {
filterRegister: UseFormRegister<FieldValues>;
setIsFilterModalOpen: (isOpen: boolean) => void;
isMutating: number;
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
search: (data: any) => void;
watch: UseFormWatch<FieldValues>;
setValue: UseFormSetValue<any>;
onClearAll: () => void;
}
function countActive(values: Record<string, unknown>): number {
let n = 0;
for (const key of ["status", "role", "type"] as const) {
const v = values[key];
if (v !== undefined && v !== null && v !== "" && String(v).trim()) n++;
}
return n;
}
const CustomerListFilters = ({
filterRegister,
setIsFilterModalOpen,
isMutating,
handleFilterSubmit,
search,
watch,
setValue,
onClearAll,
}: CustomerListFiltersProps) => {
return (
<form
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
onSubmit={handleFilterSubmit(search)}
>
<Select
{...filterRegister("status")}
items={getEnumAsArray(CustomerStatus)}
label="status"
name="status"
placeholder="Status"
clearable
value={watch("status")}
onClear={() => setValue("status", undefined)}
/>
<Select
{...filterRegister("role")}
items={getEnumAsArray(AdminRoles)}
label="role"
name="role"
placeholder="Role"
clearable
value={watch("role")}
onClear={() => setValue("role", undefined)}
/>
<Select
{...filterRegister("type")}
items={getEnumAsArray(CustomerType)}
label="type"
name="type"
placeholder="Type"
clearable
value={watch("type")}
onClear={() => setValue("type", undefined)}
/>
const watched = watch();
const activeCount = useMemo(
() => countActive(watched as Record<string, unknown>),
[watched],
);
<div className="flex gap-4 col-span-2">
<button
type="submit"
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
>
{isMutating ? (
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
) : (
<span>Search</span>
)}
</button>
<button
type="button"
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
onClick={() => setIsFilterModalOpen(false)}
>
Close
</button>
</div>
</form>
return (
<InlineListFiltersPanel
title="Filter customers"
description="Slice by status, role, or account type — inline, no popup."
activeFilterCount={activeCount}
expandAriaLabel="Expand customer filters"
collapseAriaLabel="Collapse customer filters"
panelToggleDataCy="customers-filter-panel-toggle"
>
<form
className="space-y-5 px-4 pb-6 pt-1 sm:px-6"
onSubmit={handleFilterSubmit(search)}
data-cy="customers-filter-form"
>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<Select
{...filterRegister("status")}
items={getEnumAsArray(CustomerStatus)}
label="Status"
name="status"
placeholder="Status"
clearable
value={watch("status")}
onClear={() => setValue("status", undefined)}
/>
<Select
{...filterRegister("role")}
items={getEnumAsArray(AdminRoles)}
label="Role"
name="role"
placeholder="Role"
clearable
value={watch("role")}
onClear={() => setValue("role", undefined)}
/>
<Select
{...filterRegister("type")}
items={getEnumAsArray(CustomerType)}
label="Type"
name="type"
placeholder="Type"
clearable
value={watch("type")}
onClear={() => setValue("type", undefined)}
/>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Button
type="submit"
size="small"
className="min-h-[52px] min-w-[140px] rounded-2xl bg-gradient-to-r from-primary via-primary to-violet-600 px-8 text-sm font-semibold transition hover:opacity-[0.97]"
data-cy="customers-filter-submit-button"
label={
isMutating ? (
<span className="inline-flex h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : (
<span>Apply filters</span>
)
}
/>
<button
type="button"
data-cy="customers-filter-reset-button"
onClick={onClearAll}
className="min-h-[52px] rounded-2xl border border-stroke/70 bg-white/80 px-6 text-sm font-semibold text-dark transition hover:border-primary/40 hover:text-primary dark:border-dark-3 dark:bg-dark-3/60 dark:text-white dark:hover:bg-white/[0.06]"
>
Reset all
</button>
</div>
</form>
</InlineListFiltersPanel>
);
};
+146 -154
View File
@@ -1,10 +1,11 @@
"use client";
import { ExclamationIcon, PencilSquareIcon, TrashIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import { Building } from "@/components/Layouts/sidebar/icons";
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 ListWrapper from "@/components/ui/ListWrapper";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status";
@@ -24,7 +25,6 @@ import { Tooltip } from "react-tooltip";
import AssignToCompanyModalContent from "./AssignToCompanyModalContent";
import CustomerListFilters from "./CustomerListFilters";
import useCustomerListPresenter from "./useCustomerListPresenter";
import ListWrapper from "@/components/ui/ListWrapper";
const CustomersTable = () => {
const {
@@ -44,179 +44,171 @@ const CustomersTable = () => {
setCurrentCustomer,
isAssignCompanyModalOpen,
setIsAssignCompanyModalOpen,
isFilterModalOpen,
setIsFilterModalOpen,
filterRegister,
handleFilterSubmit,
search,
watch,
setFilterValue,
isMutating,
setParams,
assignCompanyCompaniesQuery,
shouldShowPagination,
clearAllFilters,
tableSectionRef,
skeletonTbodyRef,
dataTbodyRef,
pagination,
handlePaginationChange,
} = useCustomerListPresenter();
return (
<ListWrapper>
<ListHeader
onFilter={() => setIsFilterModalOpen(true)}
createButtonText="Create Customer"
<ListHeader hasFilter={false} createButtonText="Create Customer" />
<CustomerListFilters
setValue={setFilterValue}
filterRegister={filterRegister}
handleFilterSubmit={handleFilterSubmit}
isMutating={isMutating as number}
search={search}
watch={watch}
onClearAll={clearAllFilters}
/>
<Table>
<TableHeader>
<TableRow>
{columns.map((columns) => (
<TableHead
colSpan={100}
className="text-start font-extrabold uppercase"
key={columns}
<div ref={tableSectionRef}>
<Table>
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead
colSpan={100}
className="text-start font-extrabold uppercase"
key={col}
>
{col}
</TableHead>
))}
</TableRow>
</TableHeader>
{isPending ? (
<TableSkeleton ref={skeletonTbodyRef} column={columns.length} />
) : (
<TableBody ref={dataTbodyRef}>
<EmptyListWrapper
list={allCustomers ?? []}
columns={columns}
emptyMessage="No Customers were found"
isLoading={false}
>
{columns}
</TableHead>
))}
</TableRow>
</TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody>
<EmptyListWrapper
list={allCustomers ?? []}
columns={columns}
emptyMessage="No Customers were found"
isLoading={isPending}
>
{allCustomers?.map((customer, index) => (
<TableRow key={customer?.id ?? index}>
<TableCell colSpan={100}>
{getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell>
<TableCell colSpan={100}>{customer?.full_name}</TableCell>
<TableCell colSpan={100}>{customer?.email}</TableCell>
<TableCell colSpan={100}>
{unixToDate({ unix: customer?.created_at, hasTime: true })}
</TableCell>
{allCustomers?.map((customer, index) => (
<TableRow key={customer?.id ?? index}>
<TableCell colSpan={100}>
{getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell>
<TableCell colSpan={100}>{customer?.full_name}</TableCell>
<TableCell colSpan={100}>{customer?.email}</TableCell>
<TableCell colSpan={100}>
{unixToDate({ unix: customer?.created_at, hasTime: true })}
</TableCell>
<TableCell colSpan={100} className="uppercase">
{customer?.type}
</TableCell>
<TableCell colSpan={100}>
<Status status={customer?.status}>{customer?.status}</Status>
</TableCell>
<TableCell colSpan={100}>
{customer?.companies?.length ? (
customer?.companies.map((c) => (
<span key={c.id}>{c.name}, </span>
))
) : (
<span>None</span>
)}
</TableCell>
<TableCell colSpan={100} className="uppercase">
{customer?.role ?? "-"}
</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentCustomer(customer);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Company </span>
<TrashIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() =>
router.push(`${pathName}/edit/${customer.id}`)
}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Company </span>
<PencilSquareIcon />
</button>
<button
data-tooltip-id="assign-company"
data-tooltip-content="Assign to company"
data-tooltip-place="top"
className="hover:text-green-dark"
onClick={() => {
setCurrentCustomer(customer);
setIsAssignCompanyModalOpen(true);
setSelectedCompanies({
company_ids: [customer?.companies?.[0]?.id ?? ""],
});
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "assign-company" })}
/>
<span className="sr-only">Assign To Company </span>
<Building />
</button>
<button
data-tooltip-id="feedback"
data-tooltip-content="Feedback"
data-tooltip-place="top"
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
onClick={() => {
router.push(`${pathName}/feedback/${customer.id}`);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "feedback" })} />
<ExclamationIcon />
</button>
</div>
</TableCell>
</TableRow>
))}
</EmptyListWrapper>
</TableBody>
</Table>
<TableCell colSpan={100} className="uppercase">
{customer?.type}
</TableCell>
<TableCell colSpan={100}>
<Status status={customer?.status}>{customer?.status}</Status>
</TableCell>
<TableCell colSpan={100}>
{customer?.companies?.length ? (
customer?.companies.map((c) => (
<span key={c.id}>{c.name}, </span>
))
) : (
<span>None</span>
)}
</TableCell>
<TableCell colSpan={100} className="uppercase">
{customer?.role ?? "-"}
</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentCustomer(customer);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Company </span>
<TrashIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() =>
router.push(`${pathName}/edit/${customer.id}`)
}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Company </span>
<PencilSquareIcon />
</button>
<button
data-tooltip-id="assign-company"
data-tooltip-content="Assign to company"
data-tooltip-place="top"
className="hover:text-green-dark"
onClick={() => {
setCurrentCustomer(customer);
setIsAssignCompanyModalOpen(true);
setSelectedCompanies({
company_ids: [customer?.companies?.[0]?.id ?? ""],
});
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "assign-company" })}
/>
<span className="sr-only">Assign To Company </span>
<Building />
</button>
<button
data-tooltip-id="feedback"
data-tooltip-content="Feedback"
data-tooltip-place="top"
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
onClick={() => {
router.push(`${pathName}/feedback/${customer.id}`);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "feedback" })} />
<ExclamationIcon />
</button>
</div>
</TableCell>
</TableRow>
))}
</EmptyListWrapper>
</TableBody>
)}
</Table>
</div>
<IsVisible condition={shouldShowPagination}>
<Pagination
currentPage={metadata?.page ? metadata?.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={handlePaginationChange}
/>
</IsVisible>
<Modal
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
classNames="w-full xl:w-1/2"
showButtons={false}
>
<CustomerListFilters
setValue={setFilterValue}
filterRegister={filterRegister}
handleFilterSubmit={handleFilterSubmit}
isMutating={isMutating as number}
search={search}
setIsFilterModalOpen={setIsFilterModalOpen}
watch={watch}
/>
</Modal>
<div className="w-1/12">
<Modal
classNames="w-full xl:w-1/2"
classNames="w-full xl:w-1/2"
onConfirm={() => {
if (!selectedCompanies || !currentCustomer) {
toast.error("Invalid credentials");
@@ -6,18 +6,18 @@ import {
useCustomersQuery,
useDeleteCustomerQuery,
} from "@/hooks/queries";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
import { apiDefaultParams } from "@/constants/Api_Params";
import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useForm } from "react-hook-form";
const useCustomerListPresenter = () => {
const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] =
useState(false);
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const [selectedCompanies, setSelectedCompanies] = useState<
TAssignCustomerToCompanyCredentials | undefined
>();
@@ -27,6 +27,7 @@ const useCustomerListPresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const pathName = usePathname();
const searchParams = useSearchParams();
const [params, setParams] = useState<Record<string, any>>({
...apiDefaultParams,
});
@@ -41,7 +42,31 @@ const useCustomerListPresenter = () => {
setValue: setFilterValue,
} = useForm();
const tableSectionRef = useRef<HTMLDivElement>(null);
const skeletonTbodyRef = useRef<HTMLTableSectionElement>(null);
const dataTbodyRef = useRef<HTMLTableSectionElement>(null);
useEffect(() => {
const newParams: Record<string, any> = {
...apiDefaultParams,
offset: 0,
limit: 10,
};
for (const [key, value] of searchParams.entries()) {
newParams[key] = value;
}
setParams(newParams);
filterFormReset(newParams);
}, [searchParams, filterFormReset]);
const { data, isPending } = useCustomersQuery(params);
useTableSectionRowAnimations(isPending, data, {
tableSectionRef,
skeletonTbodyRef,
dataTbodyRef,
});
const assignCompanyCompaniesQuery = useCompanyFullList({
enabled: isAssignCompanyModalOpen,
});
@@ -55,15 +80,17 @@ const useCustomerListPresenter = () => {
},
);
const search = (data: any) => {
const newParams = { ...params, ...data, offset: 0 };
const search = (formData: any) => {
const newParams = { ...params, ...formData, offset: 0 };
const cleanedParams = deleteEmptyKeys(newParams);
setParams(cleanedParams);
const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`);
setIsFilterModalOpen(false);
};
const clearAllFilters = useCallback(() => {
router.push(pathName);
}, [pathName, router]);
const columns = [
"row",
"full name",
@@ -85,6 +112,26 @@ const useCustomerListPresenter = () => {
const shouldShowPagination =
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const pagination = useMemo(
() => ({
currentPage: data?.meta?.page ? data.meta.page - 1 : 0,
totalPages: data?.meta?.pages ?? 1,
}),
[data?.meta?.page, data?.meta?.pages],
);
const handlePaginationChange = useCallback(
(e: { selected: number }) => {
const limit = data?.meta?.limit ?? 10;
setParams((currentParams) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
return {
allCustomers: customers,
metadata: data?.meta,
@@ -102,8 +149,6 @@ const useCustomerListPresenter = () => {
router,
isModalOpen,
setIsModalOpen,
isFilterModalOpen,
setIsFilterModalOpen,
filterRegister,
handleFilterSubmit,
search,
@@ -113,6 +158,12 @@ const useCustomerListPresenter = () => {
setParams,
assignCompanyCompaniesQuery,
shouldShowPagination,
clearAllFilters,
tableSectionRef,
skeletonTbodyRef,
dataTbodyRef,
pagination,
handlePaginationChange,
};
};