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:
Generated
+9
-9
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -6,4 +6,4 @@ export function createTimeFrameExtractor(
|
||||
?.split(",")
|
||||
.find((value) => value.includes(sectionKey));
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user