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,5 +1,9 @@
"use client";
import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select";
import { Button } from "@/components/ui-elements/button";
import InlineListFiltersPanel from "@/components/ui/InlineListFiltersPanel";
import {
FieldValues,
UseFormHandleSubmit,
@@ -7,66 +11,111 @@ import {
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
import { useMemo } from "react";
interface NotificationHistoryListFiltersProps {
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;
const seen = values.seen;
if (
seen !== undefined &&
seen !== null &&
seen !== "" &&
String(seen).trim()
) {
n++;
}
const searchVal = values.search;
if (typeof searchVal === "string" && searchVal.trim()) n++;
return n;
}
const NotificationHistoryListFilters = ({
filterRegister,
setIsFilterModalOpen,
isMutating,
handleFilterSubmit,
search,
watch,
setValue,
onClearAll,
}: NotificationHistoryListFiltersProps) => {
return (
<form
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
onSubmit={handleFilterSubmit(search)}
>
<Select
{...filterRegister("seen")}
items={[
{ label: "Seen", value: "true" },
{ label: "Not seen", value: "false" },
]}
label="Seen"
name="seen"
/>
<InputGroup
{...filterRegister("search")}
name="search"
label="search"
placeholder="search"
type="search"
/>
const watched = watch();
const activeCount = useMemo(
() => countActive(watched as Record<string, unknown>),
[watched],
);
<div className="flex gap-4">
<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 notifications"
description="Seen state and message search — inline, no popup."
activeFilterCount={activeCount}
expandAriaLabel="Expand notification filters"
collapseAriaLabel="Collapse notification filters"
panelToggleDataCy="notification-history-filter-panel-toggle"
>
<form
className="space-y-5 px-4 pb-6 pt-1 sm:px-6"
onSubmit={handleFilterSubmit(search)}
data-cy="notification-history-filter-form"
>
<div className="grid gap-4 sm:grid-cols-2">
<Select
{...filterRegister("seen")}
items={[
{ label: "Seen", value: "true" },
{ label: "Not seen", value: "false" },
]}
label="Seen"
name="seen"
placeholder="Any"
clearable
value={watch("seen")}
onClear={() => setValue("seen", undefined)}
/>
<InputGroup
{...filterRegister("search")}
name="search"
label="Search"
placeholder="Search"
type="search"
dataCy="notification-history-filter-search-input"
/>
</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="notification-history-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="notification-history-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>
);
};
@@ -24,7 +24,6 @@ import { Tooltip } from "react-tooltip";
import Boolean from "@/components/ui/Boolean";
import ListHeader from "@/components/ui/ListHeader";
import ListWrapper from "@/components/ui/ListWrapper";
import Modal from "@/components/ui/modal";
import NotificationHistoryListFilters from "./NotificationHistoryListFilters";
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
@@ -36,130 +35,119 @@ const NotificationHistoryTable = () => {
notificationHistory,
pathName,
router,
isFilterModalOpen,
setIsFilterModalOpen,
setParams,
handleSubmit,
register,
handleFilterSubmit,
filterRegister,
setValue,
watch,
search,
isMutating,
shouldShowPagination,
clearAllFilters,
tableSectionRef,
skeletonTbodyRef,
dataTbodyRef,
pagination,
handlePaginationChange,
} = useNotificationHistoryTablePresenter();
return (
<>
<ListWrapper>
<ListHeader
createButtonText="Create notification"
onFilter={() => setIsFilterModalOpen(true)}
<ListHeader hasFilter={false} createButtonText="Create notification" />
<NotificationHistoryListFilters
filterRegister={filterRegister}
handleFilterSubmit={handleFilterSubmit}
isMutating={isMutating as number}
watch={watch}
setValue={setValue}
search={search}
onClearAll={clearAllFilters}
/>
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead key={column}>
{column}
</TableHead>
))}
</TableRow>
</TableHeader>
{isLoading && (
<TableSkeleton column={columns.length} cellColSpan={1} />
)}
<TableBody>
<EmptyListWrapper
list={notificationHistory ?? []}
columns={columns}
emptyMessage="No notifications were found"
>
{(notificationHistory ?? []).map((item, index) => (
<TableRow key={item.id}>
<TableCell>
{getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
</TableCell>
<TableCell>{item.title}</TableCell>
<TableCell>{item.event_type}</TableCell>
<TableCell>
{unixToDate({
hasTime: true,
unix: item.created_at,
})}
</TableCell>
<TableCell>
{item.recipient?.full_name}
</TableCell>
<TableCell className="truncate">
{truncateString(item.message, 20, 20)}
</TableCell>
<TableCell className="capitalize">
{item.priority}
</TableCell>
<TableCell className="capitalize">
{item.type}
</TableCell>
<TableCell className="capitalize">
<Boolean value={item.seen} />
</TableCell>
<TableCell>
<Status status={item.status}>{item.status}</Status>
</TableCell>
<TableCell>
<button
data-tooltip-id="details"
data-tooltip-content="Details"
data-tooltip-place="top"
onClick={() => {
router.push(`${pathName}/${item.id}`);
}}
>
<Tooltip
{..._TooltipDefaultParams({
id: "details",
<div ref={tableSectionRef}>
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead key={column}>{column}</TableHead>
))}
</TableRow>
</TableHeader>
{isLoading ? (
<TableSkeleton
ref={skeletonTbodyRef}
column={columns.length}
cellColSpan={1}
/>
) : (
<TableBody ref={dataTbodyRef}>
<EmptyListWrapper
list={notificationHistory ?? []}
columns={columns}
emptyMessage="No notifications were found"
isLoading={false}
>
{(notificationHistory ?? []).map((item, index) => (
<TableRow key={item.id}>
<TableCell>
{getPaginatedRowNumber({
index,
page: metadata?.page,
limit: metadata?.limit,
})}
/>
<EyeIcon />
</button>
</TableCell>
</TableRow>
))}
</EmptyListWrapper>
</TableBody>
</Table>
</TableCell>
<TableCell>{item.title}</TableCell>
<TableCell>{item.event_type}</TableCell>
<TableCell>
{unixToDate({
hasTime: true,
unix: item.created_at,
})}
</TableCell>
<TableCell>{item.recipient?.full_name}</TableCell>
<TableCell className="truncate">
{truncateString(item.message, 20, 20)}
</TableCell>
<TableCell className="capitalize">{item.priority}</TableCell>
<TableCell className="capitalize">{item.type}</TableCell>
<TableCell className="capitalize">
<Boolean value={item.seen} />
</TableCell>
<TableCell>
<Status status={item.status}>{item.status}</Status>
</TableCell>
<TableCell>
<button
type="button"
data-tooltip-id="details"
data-tooltip-content="Details"
data-tooltip-place="top"
onClick={() => {
router.push(`${pathName}/${item.id}`);
}}
>
<Tooltip
{..._TooltipDefaultParams({
id: "details",
})}
/>
<EyeIcon />
</button>
</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: Record<string, any>) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={handlePaginationChange}
/>
</IsVisible>
</ListWrapper>
<Modal
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
showButtons={false}
>
<NotificationHistoryListFilters
filterRegister={register}
handleFilterSubmit={handleSubmit}
isMutating={isMutating}
watch={watch}
setValue={setValue}
setIsFilterModalOpen={setIsFilterModalOpen}
search={search}
/>
</Modal>
</>
);
};
@@ -1,19 +1,37 @@
"use client";
import { apiDefaultParams } from "@/constants/Api_Params";
import { useGetNotificationHistoryQuery } from "@/hooks/queries/useNotificationQueries";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
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 useNotificationHistoryTablePresenter = () => {
const { register, handleSubmit, watch, setValue } = useForm();
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const {
register: filterRegister,
handleSubmit,
watch,
setValue,
reset: filterFormReset,
} = useForm();
const [params, setParams] = useState<Record<string, any>>({
...apiDefaultParams,
});
const searchParams = useSearchParams();
const tableSectionRef = useRef<HTMLDivElement>(null);
const skeletonTbodyRef = useRef<HTMLTableSectionElement>(null);
const dataTbodyRef = useRef<HTMLTableSectionElement>(null);
const { data, isLoading } = useGetNotificationHistoryQuery(params);
useTableSectionRowAnimations(isLoading, data, {
tableSectionRef,
skeletonTbodyRef,
dataTbodyRef,
});
const shouldShowPagination =
!isLoading && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
const isMutating = useIsMutating();
@@ -32,14 +50,51 @@ const useNotificationHistoryTablePresenter = () => {
];
const pathName = usePathname();
const router = useRouter();
const search = (data: any) => {
const newParams = { ...params, ...data, offset: 0 };
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 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: Record<string, any>) => ({
...currentParams,
offset: e.selected * limit,
limit,
}));
},
[data?.meta?.limit],
);
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]);
return {
notificationHistory: data?.data,
metadata: data?.meta,
@@ -48,15 +103,20 @@ const useNotificationHistoryTablePresenter = () => {
router,
pathName,
setParams,
register,
filterRegister,
handleSubmit,
handleFilterSubmit: handleSubmit,
watch,
setValue,
isFilterModalOpen,
setIsFilterModalOpen,
isMutating,
search,
shouldShowPagination,
clearAllFilters,
tableSectionRef,
skeletonTbodyRef,
dataTbodyRef,
pagination,
handlePaginationChange,
};
};