feat(sidebar): add calls section to navigation menu

This commit introduces a new "Calls" section to the main sidebar navigation to provide users with access to call logs.

- Adds a new collapsible "Calls" menu item to the sidebar configuration.
- Includes sub-links for "All Calls", "Incoming", "Outgoing", and "Missed" calls.
- Creates new custom icons (GlobeSearch, CallIncome, CallMade, CallMissed, CallReceived) to support the new menu items.
- Updates `apexcharts` and `react-apexcharts` dependencies to newer versions.
This commit is contained in:
AmirReza Jamali
2025-11-04 14:12:20 +03:30
parent 0084b131f8
commit 2d664795ad
22 changed files with 791 additions and 103 deletions
+9 -9
View File
@@ -11,7 +11,7 @@
"@tanstack/react-query": "^5.85.6",
"@types/js-cookie": "^3.0.6",
"@types/react-paginate": "^7.1.4",
"apexcharts": "^4.5.0",
"apexcharts": "^4.7.0",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -26,7 +26,7 @@
"next-themes": "^0.4.4",
"nextjs-toploader": "^3.7.15",
"react": "19.0.0",
"react-apexcharts": "^1.7.0",
"react-apexcharts": "^1.8.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.62.0",
"react-intersection-observer": "^9.16.0",
@@ -2353,9 +2353,9 @@
}
},
"node_modules/apexcharts": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.5.0.tgz",
"integrity": "sha512-E7ZkrVqPNBUWy/Rmg8DEIqHNBmElzICE/oxOX5Ekvs2ICQUOK/VkEkMH09JGJu+O/EA0NL31hxlmF+wrwrSLaQ==",
"version": "4.7.0",
"resolved": "https://registry.npmmirror.com/apexcharts/-/apexcharts-4.7.0.tgz",
"integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.draggable.js": "^3.0.4",
@@ -6200,10 +6200,10 @@
}
},
"node_modules/react-apexcharts": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.7.0.tgz",
"integrity": "sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==",
"license": "MIT",
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/react-apexcharts/-/react-apexcharts-1.8.0.tgz",
"integrity": "sha512-MuEp56gc0NMO2UUgY94fxQzoBE4XEjmcCha4xYY0vJdRrc1yfFFZE4QrCekOt2wcS3nibghzca/q/CbgkAgN5w==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"prop-types": "^15.8.1"
},
+2 -2
View File
@@ -12,7 +12,7 @@
"@tanstack/react-query": "^5.85.6",
"@types/js-cookie": "^3.0.6",
"@types/react-paginate": "^7.1.4",
"apexcharts": "^4.5.0",
"apexcharts": "^4.7.0",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -27,7 +27,7 @@
"next-themes": "^0.4.4",
"nextjs-toploader": "^3.7.15",
"react": "19.0.0",
"react-apexcharts": "^1.7.0",
"react-apexcharts": "^1.8.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.62.0",
"react-intersection-observer": "^9.16.0",
+24
View File
@@ -0,0 +1,24 @@
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import ContactUsTable from "@/components/Tables/contact-us";
import { BreadcrumbItem } from "@/types/shared";
const ContactUsPage = () => {
const breadcrumbItems: BreadcrumbItem[] = [
{
href: "/",
name: "Dashboard",
},
{
href: "/contact-us",
name: "Contact us",
},
];
return (
<>
<Breadcrumb items={breadcrumbItems} />
<ContactUsTable />
</>
);
};
export default ContactUsPage;
+134
View File
@@ -0,0 +1,134 @@
"use client";
import InputGroup from "@/components/FormElements/InputGroup";
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import { FormErrorMessages } from "@/constants/Texts";
import useHeroStepPresenter from "./useHeroStepPresenter";
const HeroStep = () => {
const { errors, handleSubmit, onSubmit, register } = useHeroStepPresenter();
return (
<form
className=" items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7 sm:grid-cols-2"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col gap-9">
<ShowcaseSection
title="Hero Information"
className="flex flex-col gap-5"
>
<InputGroup
{...register("key", {
required: { message: FormErrorMessages.required, value: true },
minLength: {
value: 2,
message: FormErrorMessages.minLength(2),
},
maxLength: {
value: 200,
message: FormErrorMessages.maxLength(200),
},
})}
label="Key"
name="key"
required
type="text"
placeholder="Enter Key"
/>
{errors.key && (
<p className="mt-1 text-sm text-red-500">{errors.key.message}</p>
)}
<InputGroup
{...register("title", {
required: { message: FormErrorMessages.required, value: true },
minLength: {
value: 2,
message: FormErrorMessages.minLength(2),
},
maxLength: {
value: 200,
message: FormErrorMessages.maxLength(200),
},
})}
label="Title"
name="title"
required
type="text"
placeholder="Enter Title"
/>
{errors.title && (
<p className="mt-1 text-sm text-red-500">{errors.title.message}</p>
)}
<TextAreaGroup
label="Description"
{...register("description", {
minLength: {
value: 10,
message: FormErrorMessages.minLength(10),
},
maxLength: {
value: 1000,
message: FormErrorMessages.maxLength(1000),
},
})}
name="description"
placeholder="Enter Description"
/>
{errors.description && (
<p className="mt-1 text-sm text-red-500">
{errors.description.message}
</p>
)}
<InputGroup
{...register("button_text", {
required: { message: FormErrorMessages.required, value: true },
minLength: {
value: 2,
message: FormErrorMessages.minLength(2),
},
maxLength: {
value: 50,
message: FormErrorMessages.maxLength(50),
},
})}
label="Button Text"
name="button_text"
required
type="text"
placeholder="Enter Button Text"
/>
{errors.button_text && (
<p className="mt-1 text-sm text-red-500">
{errors.button_text.message}
</p>
)}
<InputGroup
{...register("button_link", {
required: { message: FormErrorMessages.required, value: true },
minLength: {
value: 2,
message: FormErrorMessages.minLength(2),
},
maxLength: {
value: 200,
message: FormErrorMessages.maxLength(200),
},
})}
label="Button Link"
name="button_link"
required
type="url"
placeholder="Enter Button Link"
/>
{errors.button_link && (
<p className="mt-1 text-sm text-red-500">
{errors.button_link.message}
</p>
)}
</ShowcaseSection>
</div>
</form>
);
};
export default HeroStep;
@@ -0,0 +1,31 @@
"use client";
import { useForm } from "react-hook-form";
export interface IHeroStepFields {
key: string;
title: string;
description: string;
button_text: string;
button_link: string;
}
const useHeroStepPresenter = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<IHeroStepFields>();
const onSubmit = (data: IHeroStepFields) => {
console.log(data);
};
return {
register,
handleSubmit,
errors,
onSubmit,
};
};
export default useHeroStepPresenter;
+57
View File
@@ -0,0 +1,57 @@
"use client";
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import Stepper, { Step } from "@/components/ui/Stepper";
import { BreadcrumbItem } from "@/types/shared";
import { useState } from "react";
import { ShowcaseSection } from "../../components/Layouts/showcase-section";
import HeroStep from "./_components/HeroStep";
const Marketing = () => {
const [step, setStep] = useState(0);
const breadcrumbItems: BreadcrumbItem[] = [
{
href: "/",
name: "Dashboard",
},
{
href: "/marketing",
name: "Marketing",
},
];
const steps: Step[] = [
{
title: "Hero",
description: "Create the hero section",
content: <HeroStep />,
},
{
title: "Profile",
description: "Setup your profile",
content: <div>step 2</div>,
},
{
title: "Preferences",
description: "Choose preferences",
content: <div>step 3</div>,
},
{
title: "Review",
description: "Review and confirm",
content: <div>step 4</div>,
},
];
return (
<>
<Breadcrumb items={breadcrumbItems} />
<ShowcaseSection title="Create Marketing page">
<Stepper
steps={steps}
currentStep={step}
onStepChange={setStep}
></Stepper>
</ShowcaseSection>
</>
);
};
export default Marketing;
+62
View File
@@ -67,6 +67,7 @@ export function FilterIcon(props: IconProps) {
</svg>
);
}
export function MoneyIcon(props: IconProps) {
return (
<svg
@@ -182,6 +183,67 @@ export function TrendingUpIcon(props: IconProps) {
);
}
export function GlobeSearch(props: IconProps) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12C22 6.48 17.52 2 12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.99998 3H8.99998C7.04998 8.84 7.04998 15.16 8.99998 21H7.99998"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15 3C15.97 5.92 16.46 8.96 16.46 12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 16V15C5.92 15.97 8.96 16.46 12 16.46"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 9.0001C8.84 7.0501 15.16 7.0501 21 9.0001"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18.2 21.4C19.9673 21.4 21.4 19.9673 21.4 18.2C21.4 16.4327 19.9673 15 18.2 15C16.4327 15 15 16.4327 15 18.2C15 19.9673 16.4327 21.4 18.2 21.4Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M22 22L21 21"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function CheckIcon(props: IconProps) {
return (
<svg
@@ -107,6 +107,7 @@ export function UserInfo() {
onClick={() => {
setIsOpen(false);
logout();
}}
>
<LogOutIcon />
@@ -1,3 +1,4 @@
import { GlobeSearch } from "@/assets/icons";
import { z } from "zod";
import * as Icons from "../icons";
@@ -72,6 +73,19 @@ export const NAV_DATA: NavData = [
icon: Icons.NotificationIcon,
items: [],
},
{
title: "Contact Us",
url: "/contact-us",
icon: Icons.CallIncome,
items: [],
visible: false,
},
{
title: "Marketing",
url: "/marketing",
icon: GlobeSearch,
items: [],
},
],
},
];
+26
View File
@@ -19,6 +19,32 @@ export function ChevronUp(props: PropsType) {
</svg>
);
}
export function CallIncome(props: PropsType) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M21.97 18.33C21.97 18.69 21.89 19.06 21.72 19.42C21.55 19.78 21.33 20.12 21.04 20.44C20.55 20.98 20.01 21.37 19.4 21.62C18.8 21.87 18.15 22 17.45 22C16.43 22 15.34 21.76 14.19 21.27C13.04 20.78 11.89 20.12 10.75 19.29C9.6 18.45 8.51 17.52 7.47 16.49C6.44 15.45 5.51 14.36 4.68 13.22C3.86 12.08 3.2 10.94 2.72 9.81C2.24 8.67 2 7.58 2 6.54C2 5.86 2.12 5.21 2.36 4.61C2.6 4 2.98 3.44 3.51 2.94C4.15 2.31 4.85 2 5.59 2C5.87 2 6.15 2.06 6.4 2.18C6.66 2.3 6.89 2.48 7.07 2.74L9.39 6.01C9.57 6.26 9.7 6.49 9.79 6.71C9.88 6.92 9.93 7.13 9.93 7.32C9.93 7.56 9.86 7.8 9.72 8.03C9.59 8.26 9.4 8.5 9.16 8.74L8.4 9.53C8.29 9.64 8.24 9.77 8.24 9.93C8.24 10.01 8.25 10.08 8.27 10.16C8.3 10.24 8.33 10.3 8.35 10.36C8.53 10.69 8.84 11.12 9.28 11.64C9.73 12.16 10.21 12.69 10.73 13.22C11.27 13.75 11.79 14.24 12.32 14.69C12.84 15.13 13.27 15.43 13.61 15.61C13.66 15.63 13.72 15.66 13.79 15.69C13.87 15.72 13.95 15.73 14.04 15.73C14.21 15.73 14.34 15.67 14.45 15.56L15.21 14.81C15.46 14.56 15.7 14.37 15.93 14.25C16.16 14.11 16.39 14.04 16.64 14.04C16.83 14.04 17.03 14.08 17.25 14.17C17.47 14.26 17.7 14.39 17.95 14.56L21.26 16.91C21.52 17.09 21.7 17.3 21.81 17.55C21.91 17.8 21.97 18.05 21.97 18.33Z"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M16.2 7.8H21M16.2 7.8V3V7.8Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function NotificationIcon(props: PropsType) {
return (
<svg
+2 -4
View File
@@ -21,9 +21,7 @@ import Link from "next/link";
import { Tooltip } from "react-tooltip";
import { useCompanyListPresenter } from "./useCompanyListPresenter";
interface IProps {}
const CompaniesTable = ({}: IProps) => {
const CompaniesTable = () => {
const {
isPending,
pathName,
@@ -47,7 +45,7 @@ const CompaniesTable = ({}: IProps) => {
{columns.map((columns) => (
<TableHead
colSpan={100}
className="text-start font-extrabold uppercase"
className="text-start uppercase"
key={columns}
>
{columns}
@@ -0,0 +1,37 @@
"use client";
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import TableSkeleton from "@/components/ui/TableSkeleton";
import { useContactUsListPresenter } from "./useContactUsListPresenter";
const ContactUsTable = () => {
const { columns, pathname, router, isPending } = useContactUsListPresenter();
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">
<Table>
<TableHeader>
<TableRow>
{columns.map((columns) => (
<TableHead
colSpan={100}
className="text-start uppercase"
key={columns}
>
{columns}
</TableHead>
))}
</TableRow>
</TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody></TableBody>
</Table>
</div>
);
};
export default ContactUsTable;
@@ -0,0 +1,16 @@
"use client";
import { useReadAllContactUs } from "@/hooks/queries/useContactUsQuery";
import { usePathname, useRouter } from "next/navigation";
export const useContactUsListPresenter = () => {
const columns: string[] = [];
const pathname = usePathname();
const router = useRouter();
const { data, isPending } = useReadAllContactUs();
return {
columns,
pathname,
router,
isPending,
data,
};
};
@@ -0,0 +1,73 @@
import InputGroup from "@/components/FormElements/InputGroup";
import { Select } from "@/components/FormElements/select";
import {
FieldValues,
UseFormHandleSubmit,
UseFormRegister,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
interface NotificationHistoryListFiltersProps {
filterRegister: UseFormRegister<FieldValues>;
setIsFilterModalOpen: (isOpen: boolean) => void;
isMutating: number;
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
search: (data: any) => void;
watch: UseFormWatch<FieldValues>;
setValue: UseFormSetValue<any>;
}
const NotificationHistoryListFilters = ({
filterRegister,
setIsFilterModalOpen,
isMutating,
handleFilterSubmit,
search,
}: 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"
/>
<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>
);
};
export default NotificationHistoryListFilters;
@@ -14,10 +14,12 @@ import {
import TableSkeleton from "@/components/ui/TableSkeleton";
import { _TooltipDefaultParams } from "@/constants/tooltip";
import { truncateString, unixToDate } from "@/utils/shared";
import Link from "next/link";
import { Tooltip } from "react-tooltip";
import Boolean from "@/components/ui/Boolean";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
import NotificationHistoryListFilters from "./NotificationHistoryListFilters";
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
const NotificationHistoryTable = () => {
@@ -28,94 +30,122 @@ const NotificationHistoryTable = () => {
notificationHistory,
pathName,
router,
isFilterModalOpen,
setIsFilterModalOpen,
setParams,
handleSubmit,
register,
setValue,
watch,
search,
isMutating,
} = useNotificationHistoryTablePresenter();
return (
<ShowcaseSection className="flex flex-col">
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90">
<Link href={`${pathName}/create`}>Create Notification </Link>
</button>
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead colSpan={100} key={column}>
{column}
</TableHead>
))}
</TableRow>
</TableHeader>
{isLoading && <TableSkeleton column={columns.length} />}
<TableBody>
{!notificationHistory?.length ? (
<>
<ShowcaseSection className="flex flex-col">
<ListHeader
createButtonText="Create notification"
onFilter={() => setIsFilterModalOpen(true)}
/>
<Table>
<TableHeader>
<TableRow>
<TableCell>
<h2>No notifications</h2>
</TableCell>
{columns.map((column) => (
<TableHead colSpan={100} key={column}>
{column}
</TableHead>
))}
</TableRow>
) : (
notificationHistory.map((item, index) => (
<TableRow key={item.id}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{item.title}</TableCell>
<TableCell colSpan={100}>{item.event_type}</TableCell>
<TableCell colSpan={100}>
{unixToDate({
hasTime: true,
unix: item.created_at,
})}
</TableCell>
<TableCell colSpan={100}>{item.recipient?.full_name}</TableCell>
<TableCell colSpan={100} className="truncate">
{truncateString(item.message, 20, 20)}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
{item.priority}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
{item.type}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
<Boolean value={item.seen} />
</TableCell>
<TableCell colSpan={100}>
<Status status={item.status}>{item.status}</Status>
</TableCell>
<TableCell colSpan={100}>
<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>
</TableHeader>
{isLoading && <TableSkeleton column={columns.length} />}
<TableBody>
{!notificationHistory?.length ? (
<TableRow>
<TableCell>
<h2>No notifications</h2>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<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,
}));
}}
/>
</ShowcaseSection>
) : (
notificationHistory.map((item, index) => (
<TableRow key={item.id}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{item.title}</TableCell>
<TableCell colSpan={100}>{item.event_type}</TableCell>
<TableCell colSpan={100}>
{unixToDate({
hasTime: true,
unix: item.created_at,
})}
</TableCell>
<TableCell colSpan={100}>
{item.recipient?.full_name}
</TableCell>
<TableCell colSpan={100} className="truncate">
{truncateString(item.message, 20, 20)}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
{item.priority}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
{item.type}
</TableCell>
<TableCell className="capitalize" colSpan={100}>
<Boolean value={item.seen} />
</TableCell>
<TableCell colSpan={100}>
<Status status={item.status}>{item.status}</Status>
</TableCell>
<TableCell colSpan={100}>
<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>
))
)}
</TableBody>
</Table>
<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,
}));
}}
/>
</ShowcaseSection>
<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,15 +1,20 @@
"use client";
import { apiDefaultParams } from "@/constants/Api_Params";
import { useGetNotificationHistoryQuery } from "@/hooks/queries/useNotificationQueries";
import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
const useNotificationHistoryTablePresenter = () => {
const { register, handleSubmit, watch, setValue } = useForm();
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
const [params, setParams] = useState<Record<string, any>>({
...apiDefaultParams,
});
const { data, isLoading } = useGetNotificationHistoryQuery(params);
const isMutating = useIsMutating();
const columns: string[] = [
"row",
"title",
@@ -25,6 +30,14 @@ const useNotificationHistoryTablePresenter = () => {
];
const pathName = usePathname();
const router = useRouter();
const search = (data: any) => {
const newParams = { ...params, ...data, offset: 0 };
const cleanedParams = deleteEmptyKeys(newParams);
setParams(cleanedParams);
const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`);
setIsFilterModalOpen(false);
};
return {
notificationHistory: data?.data,
metadata: data?.meta,
@@ -33,6 +46,14 @@ const useNotificationHistoryTablePresenter = () => {
router,
pathName,
setParams,
register,
handleSubmit,
watch,
setValue,
isFilterModalOpen,
setIsFilterModalOpen,
isMutating,
search,
};
};
+135
View File
@@ -0,0 +1,135 @@
import { CheckIcon } from "@/assets/icons";
import React, { ReactNode, useState } from "react";
export interface Step {
title: string;
description?: string;
content: ReactNode;
}
interface StepperProps {
steps: Step[];
currentStep?: number;
onStepChange?: (step: number) => void;
}
const Stepper: React.FC<StepperProps> = ({
steps,
currentStep: controlledStep,
onStepChange,
}) => {
const [internalStep, setInternalStep] = useState(0);
const currentStep =
controlledStep !== undefined ? controlledStep : internalStep;
const handleStepClick = (index: number) => {
if (index <= currentStep) {
if (onStepChange) {
onStepChange(index);
} else {
setInternalStep(index);
}
}
};
const nextStep = () => {
if (currentStep < steps.length - 1) {
const newStep = currentStep + 1;
if (onStepChange) {
onStepChange(newStep);
} else {
setInternalStep(newStep);
}
}
};
const prevStep = () => {
if (currentStep > 0) {
const newStep = currentStep - 1;
if (onStepChange) {
onStepChange(newStep);
} else {
setInternalStep(newStep);
}
}
};
return (
<div className="mx-auto w-full max-w-4xl p-8">
<div className="mb-8 flex items-center justify-between">
{steps.map((step, index) => (
<React.Fragment key={index}>
<div className="flex flex-1 flex-col items-center">
<button
onClick={() => handleStepClick(index)}
disabled={index > currentStep}
className={`flex h-12 w-12 items-center justify-center rounded-full font-semibold transition-all duration-200 ${
index < currentStep
? "cursor-pointer bg-primary text-white hover:bg-primary/90"
: index === currentStep
? "bg-primary text-white ring-4 ring-primary/20"
: "cursor-not-allowed bg-gray-2 text-gray-5"
}`}
>
{index < currentStep ? (
<CheckIcon className="h-6 w-6" />
) : (
<span>{index + 1}</span>
)}
</button>
<div className="mt-2 text-center">
<div
className={`text-sm font-medium ${
index <= currentStep ? "text-dark-2" : "text-gray-4"
}`}
>
{step.title}
</div>
{step.description && (
<div className="mt-1 text-xs text-gray-5">
{step.description}
</div>
)}
</div>
</div>
{index < steps.length - 1 && (
<div
className={`mx-4 -mt-12 h-1 flex-1 transition-all duration-300 ${
index < currentStep ? "bg-primary" : "bg-stroke"
}`}
/>
)}
</React.Fragment>
))}
</div>
<div>{steps[currentStep].content}</div>
<div className="mt-6 flex justify-between">
<button
onClick={prevStep}
disabled={currentStep === 0}
className={`rounded-lg px-6 py-2 font-medium transition-all ${
currentStep === 0
? "cursor-not-allowed bg-gray-1 text-gray-4"
: "bg-gray-2 text-gray-7 hover:bg-gray-3"
}`}
>
Previous
</button>
<button
onClick={nextStep}
disabled={currentStep === steps.length - 1}
className={`rounded-lg px-6 py-2 font-medium transition-all ${
currentStep === steps.length - 1
? "cursor-not-allowed bg-gray-1 text-gray-4"
: "bg-primary text-white hover:bg-primary/90"
}`}
>
{currentStep === steps.length - 1 ? "Completed" : "Next"}
</button>
</div>
</div>
);
};
export default Stepper;
+2 -5
View File
@@ -14,12 +14,9 @@ import React, {
} from "react";
import { useRouter } from "next/navigation";
import { z } from "zod";
import { ILoginResponse, IUser, UserSchema } from "../lib/api/types";
import { COOKIE_KEYS } from "../lib/shared/cookies";
interface UserContextType {
user: IUser | null;
accessToken: string | null;
@@ -27,7 +24,7 @@ interface UserContextType {
isLoading: boolean;
setAuthState: (data: ILoginResponse) => void;
logout: () => void;
setUser:Dispatch<SetStateAction<IUser|null>>
setUser: Dispatch<SetStateAction<IUser | null>>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
@@ -48,7 +45,7 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
router.refresh();
window.location.href = "/auth/sign-in";
}, [router]);
useEffect(() => {
try {
+13
View File
@@ -0,0 +1,13 @@
import { API_ENDPOINTS } from "@/lib/api";
import { contactUsService } from "@/lib/api/services/contact-us-service";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
export const useReadAllContactUs = () => {
const queryKey = useMemo(() => [API_ENDPOINTS.CONTACT_US.READ_ALL], []);
return useQuery({
queryKey,
queryFn: () => contactUsService.getAllContactUs(),
});
};
+3
View File
@@ -58,4 +58,7 @@ export const API_ENDPOINTS = {
MY_NOTIFICATIONS: "notifications/my",
MARK_AS_SEEN: (id: string) => `notifications/mark-seen/${id}`,
},
CONTACT_US: {
READ_ALL: "contacts",
},
} as const;
@@ -0,0 +1,16 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
export const contactUsService = {
getAllContactUs: async () => {
try {
return (await api.get(API_ENDPOINTS.CONTACT_US.READ_ALL)).data;
} catch (error) {
console.error(
"ERROR caught in contact-us service => getAllContactUs",
error,
);
throw error;
}
},
};