Initial commit for new panel
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { GoogleIcon } from "@/assets/icons";
|
||||
|
||||
export default function GoogleSigninButton({ text }: { text: string }) {
|
||||
return (
|
||||
<button className="flex w-full items-center justify-center gap-3.5 rounded-lg border border-stroke bg-gray-2 p-[15px] font-medium hover:bg-opacity-50 dark:border-dark-3 dark:bg-dark-2 dark:hover:bg-opacity-50">
|
||||
<GoogleIcon />
|
||||
{text} with Google
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Link from "next/link";
|
||||
import GoogleSigninButton from "../GoogleSigninButton";
|
||||
import SigninWithPassword from "../SigninWithPassword";
|
||||
|
||||
export default function Signin() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<SigninWithPassword />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
import { PasswordIcon, UserIcon } from "@/assets/icons";
|
||||
|
||||
import InputGroup from "../FormElements/InputGroup";
|
||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||
import { ILoginCredentials } from "@/lib/api";
|
||||
import { useLoginQuery } from "@/hooks/queries";
|
||||
|
||||
export default function SigninWithPassword() {
|
||||
const { handleSubmit, control, reset } = useForm<ILoginCredentials>();
|
||||
const { mutate, isPending } = useLoginQuery();
|
||||
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
||||
mutate(data);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name="username"
|
||||
control={control}
|
||||
render={({ field: username }) => (
|
||||
<InputGroup
|
||||
type="username"
|
||||
handleChange={(value) => username.onChange(value)}
|
||||
label="Username"
|
||||
className="mb-4 [&_input]:py-[15px]"
|
||||
placeholder="Enter your Username"
|
||||
name="username"
|
||||
icon={<UserIcon />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field: password }) => (
|
||||
<InputGroup
|
||||
type="password"
|
||||
handleChange={(value) => password.onChange(value)}
|
||||
label="Password"
|
||||
className="mb-5 [&_input]:py-[15px]"
|
||||
placeholder="Enter your password"
|
||||
name="password"
|
||||
icon={<PasswordIcon />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mb-4.5">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary p-4 font-medium text-white transition hover:bg-opacity-90"
|
||||
>
|
||||
Sign In
|
||||
{/* {loading && (
|
||||
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-white border-t-transparent dark:border-primary dark:border-t-transparent" />
|
||||
)} */}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Link from "next/link";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
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">
|
||||
{currentPage.name}
|
||||
</h2>
|
||||
|
||||
<nav>
|
||||
<ol className="flex items-center gap-2">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
|
||||
return isLast ? (
|
||||
<li key={index} className="font-medium text-primary">
|
||||
{item.name}
|
||||
</li>
|
||||
) : (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<Link
|
||||
href={item.href}
|
||||
className="font-medium hover:text-primary"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
<span className="text-dark dark:text-white">/</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Breadcrumb;
|
||||
@@ -0,0 +1,237 @@
|
||||
const CalendarBox = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full max-w-full rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="grid grid-cols-7 rounded-t-[10px] bg-primary text-white">
|
||||
<th className="flex h-15 items-center justify-center rounded-tl-[10px] p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Sunday </span>
|
||||
<span className="block lg:hidden"> Sun </span>
|
||||
</th>
|
||||
<th className="flex h-15 items-center justify-center p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Monday </span>
|
||||
<span className="block lg:hidden"> Mon </span>
|
||||
</th>
|
||||
<th className="flex h-15 items-center justify-center p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Tuesday </span>
|
||||
<span className="block lg:hidden"> Tue </span>
|
||||
</th>
|
||||
<th className="flex h-15 items-center justify-center p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Wednesday </span>
|
||||
<span className="block lg:hidden"> Wed </span>
|
||||
</th>
|
||||
<th className="flex h-15 items-center justify-center p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Thursday </span>
|
||||
<span className="block lg:hidden"> Thur </span>
|
||||
</th>
|
||||
<th className="flex h-15 items-center justify-center p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Friday </span>
|
||||
<span className="block lg:hidden"> Fri </span>
|
||||
</th>
|
||||
<th className="flex h-15 items-center justify-center rounded-tr-[10px] p-1 text-body-xs font-medium sm:text-base xl:p-5">
|
||||
<span className="hidden lg:block"> Saturday </span>
|
||||
<span className="block lg:hidden"> Sat </span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* <!-- Line 1 --> */}
|
||||
<tr className="grid grid-cols-7">
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">1</span>
|
||||
<div className="group h-16 w-full flex-grow cursor-pointer py-1 md:h-30">
|
||||
<span className="group-hover:text-primary md:hidden">
|
||||
More
|
||||
</span>
|
||||
<div className="event invisible absolute left-2 z-99 mb-1 flex w-[200%] flex-col rounded-r-[5px] border-l-[3px] border-primary bg-gray-2 px-3 py-1 text-left opacity-0 group-hover:visible group-hover:opacity-100 dark:bg-dark-2 md:visible md:w-[190%] md:opacity-100">
|
||||
<span className="event-name font-medium text-dark dark:text-white">
|
||||
Redesign Website
|
||||
</span>
|
||||
<span className="time text-sm">1 Dec - 2 Dec</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">2</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">3</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">4</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">5</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">6</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">7</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/* <!-- Line 1 --> */}
|
||||
{/* <!-- Line 2 --> */}
|
||||
<tr className="grid grid-cols-7">
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">8</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">9</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
10
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
11
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
12
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
13
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
14
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/* <!-- Line 2 --> */}
|
||||
{/* <!-- Line 3 --> */}
|
||||
<tr className="grid grid-cols-7">
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
15
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
16
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
17
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
18
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
19
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
20
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
21
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/* <!-- Line 3 --> */}
|
||||
{/* <!-- Line 4 --> */}
|
||||
<tr className="grid grid-cols-7">
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
22
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
23
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
24
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
25
|
||||
</span>
|
||||
<div className="group h-16 w-full flex-grow cursor-pointer py-1 md:h-30">
|
||||
<span className="group-hover:text-primary md:hidden">
|
||||
More
|
||||
</span>
|
||||
<div className="event invisible absolute left-2 z-99 mb-1 flex w-[300%] flex-col rounded-r-[5px] border-l-[3px] border-primary bg-gray-2 px-3 py-1 text-left opacity-0 group-hover:visible group-hover:opacity-100 dark:bg-dark-2 md:visible md:w-[290%] md:opacity-100">
|
||||
<span className="event-name font-medium text-dark dark:text-white">
|
||||
App Design
|
||||
</span>
|
||||
<span className="time text-sm">25 Dec - 27 Dec</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
26
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
27
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
28
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/* <!-- Line 4 --> */}
|
||||
{/* <!-- Line 5 --> */}
|
||||
<tr className="grid grid-cols-7">
|
||||
<td className="ease relative h-20 cursor-pointer rounded-bl-[10px] border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
29
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
30
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">
|
||||
31
|
||||
</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">1</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">2</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">3</span>
|
||||
</td>
|
||||
<td className="ease relative h-20 cursor-pointer rounded-br-[10px] border border-stroke p-2 transition duration-500 hover:bg-gray-2 dark:border-dark-3 dark:hover:bg-dark-2 md:h-25 md:p-6 xl:h-31">
|
||||
<span className="font-medium text-dark dark:text-white">4</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/* <!-- Line 5 --> */}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarBox;
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import type { ApexOptions } from "apexcharts";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type PropsType = {
|
||||
data: {
|
||||
x: string;
|
||||
y: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
const Chart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function CampaignVisitorsChart({ data }: PropsType) {
|
||||
const options: ApexOptions = {
|
||||
colors: ["#5750F1"],
|
||||
chart: {
|
||||
fontFamily: "Satoshi, sans-serif",
|
||||
type: "bar",
|
||||
height: 200,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: "40%",
|
||||
borderRadius: 3,
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 4,
|
||||
colors: ["transparent"],
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
fontFamily: "Satoshi",
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 7,
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="-ml-3.5 px-6 pb-1 pt-7.5">
|
||||
<Chart
|
||||
options={options}
|
||||
series={[
|
||||
{
|
||||
name: "Visitors",
|
||||
data,
|
||||
},
|
||||
]}
|
||||
type="bar"
|
||||
height={230}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { TrendingUpIcon } from "@/assets/icons";
|
||||
import { compactFormat } from "@/lib/format-number";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getCampaignVisitorsData } from "@/services/charts.services";
|
||||
import { CampaignVisitorsChart } from "./chart";
|
||||
|
||||
export async function CampaignVisitors({ className }: { className?: string }) {
|
||||
const data = await getCampaignVisitorsData();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="border-b border-stroke px-6 py-5.5 dark:border-dark-3">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="mb-1.5 text-2xl font-bold text-dark dark:text-white">
|
||||
Campaign Visitors
|
||||
</h2>
|
||||
|
||||
<div className="mb-0.5 text-2xl font-bold text-dark dark:text-white">
|
||||
{compactFormat(data.total_visitors)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="text-sm font-medium">Last Campaign Performance</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5",
|
||||
data.performance > 0 ? "text-green" : "text-red",
|
||||
)}
|
||||
>
|
||||
<TrendingUpIcon
|
||||
className={`${data.performance > 0 ? "-rotate-6" : "scale-y-[-1]"}`}
|
||||
/>
|
||||
|
||||
<span className="text-sm font-medium">{data.performance}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CampaignVisitorsChart data={data.chart} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import type { ApexOptions } from "apexcharts";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type PropsType = {
|
||||
data: {
|
||||
received: { x: unknown; y: number }[];
|
||||
due: { x: unknown; y: number }[];
|
||||
};
|
||||
};
|
||||
|
||||
const Chart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function PaymentsOverviewChart({ data }: PropsType) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const options: ApexOptions = {
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
colors: ["#5750F1", "#0ABEF9"],
|
||||
chart: {
|
||||
height: 310,
|
||||
type: "area",
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
fontFamily: "inherit",
|
||||
},
|
||||
fill: {
|
||||
gradient: {
|
||||
opacityFrom: 0.55,
|
||||
opacityTo: 0,
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1024,
|
||||
options: {
|
||||
chart: {
|
||||
height: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 1366,
|
||||
options: {
|
||||
chart: {
|
||||
height: 320,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
stroke: {
|
||||
curve: "smooth",
|
||||
width: isMobile ? 2 : 3,
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 5,
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
tooltip: {
|
||||
marker: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="-ml-4 -mr-5 h-[310px]">
|
||||
<Chart
|
||||
options={options}
|
||||
series={[
|
||||
{
|
||||
name: "Received",
|
||||
data: data.received,
|
||||
},
|
||||
{
|
||||
name: "Due",
|
||||
data: data.due,
|
||||
},
|
||||
]}
|
||||
type="area"
|
||||
height={310}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { PeriodPicker } from "@/components/period-picker";
|
||||
import { standardFormat } from "@/lib/format-number";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getPaymentsOverviewData } from "@/services/charts.services";
|
||||
import { PaymentsOverviewChart } from "./chart";
|
||||
|
||||
type PropsType = {
|
||||
timeFrame?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export async function PaymentsOverview({
|
||||
timeFrame = "monthly",
|
||||
className,
|
||||
}: PropsType) {
|
||||
const data = await getPaymentsOverviewData(timeFrame);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-2 rounded-[10px] bg-white px-7.5 pb-6 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 className="text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Payments Overview
|
||||
</h2>
|
||||
|
||||
<PeriodPicker defaultValue={timeFrame} sectionKey="payments_overview" />
|
||||
</div>
|
||||
|
||||
<PaymentsOverviewChart data={data} />
|
||||
|
||||
<dl className="grid divide-stroke text-center dark:divide-dark-3 sm:grid-cols-2 sm:divide-x [&>div]:flex [&>div]:flex-col-reverse [&>div]:gap-1">
|
||||
<div className="dark:border-dark-3 max-sm:mb-3 max-sm:border-b max-sm:pb-3">
|
||||
<dt className="text-xl font-bold text-dark dark:text-white">
|
||||
${standardFormat(data.received.reduce((acc, { y }) => acc + y, 0))}
|
||||
</dt>
|
||||
<dd className="font-medium dark:text-dark-6">Received Amount</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="text-xl font-bold text-dark dark:text-white">
|
||||
${standardFormat(data.due.reduce((acc, { y }) => acc + y, 0))}
|
||||
</dt>
|
||||
<dd className="font-medium dark:text-dark-6">Due Amount</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { compactFormat } from "@/lib/format-number";
|
||||
import type { ApexOptions } from "apexcharts";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type PropsType = {
|
||||
data: { name: string; amount: number }[];
|
||||
};
|
||||
|
||||
const Chart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function DonutChart({ data }: PropsType) {
|
||||
const chartOptions: ApexOptions = {
|
||||
chart: {
|
||||
type: "donut",
|
||||
fontFamily: "inherit",
|
||||
},
|
||||
colors: ["#5750F1", "#5475E5", "#8099EC", "#ADBCF2"],
|
||||
labels: data.map((item) => item.name),
|
||||
legend: {
|
||||
show: true,
|
||||
position: "bottom",
|
||||
itemMargin: {
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
},
|
||||
formatter: (legendName, opts) => {
|
||||
const { seriesPercent } = opts.w.globals;
|
||||
return `${legendName}: ${seriesPercent[opts.seriesIndex]}%`;
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: "80%",
|
||||
background: "transparent",
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
showAlways: true,
|
||||
label: "Visitors",
|
||||
fontSize: "16px",
|
||||
fontWeight: "400",
|
||||
},
|
||||
value: {
|
||||
show: true,
|
||||
fontSize: "28px",
|
||||
fontWeight: "bold",
|
||||
formatter: (val) => compactFormat(+val),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 2600,
|
||||
options: {
|
||||
chart: {
|
||||
width: 415,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 640,
|
||||
options: {
|
||||
chart: {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 370,
|
||||
options: {
|
||||
chart: {
|
||||
width: 260,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Chart
|
||||
options={chartOptions}
|
||||
series={data.map((item) => item.amount)}
|
||||
type="donut"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { PeriodPicker } from "@/components/period-picker";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getDevicesUsedData } from "@/services/charts.services";
|
||||
import { DonutChart } from "./chart";
|
||||
|
||||
type PropsType = {
|
||||
timeFrame?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export async function UsedDevices({
|
||||
timeFrame = "monthly",
|
||||
className,
|
||||
}: PropsType) {
|
||||
const data = await getDevicesUsedData(timeFrame);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 grid-rows-[auto_1fr] gap-9 rounded-[10px] bg-white p-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 className="text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Used Devices
|
||||
</h2>
|
||||
|
||||
<PeriodPicker defaultValue={timeFrame} sectionKey="used_devices" />
|
||||
</div>
|
||||
|
||||
<div className="grid place-items-center">
|
||||
<DonutChart data={data} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import type { ApexOptions } from "apexcharts";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type PropsType = {
|
||||
data: {
|
||||
sales: { x: string; y: number }[];
|
||||
revenue: { x: string; y: number }[];
|
||||
};
|
||||
};
|
||||
|
||||
const Chart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function WeeksProfitChart({ data }: PropsType) {
|
||||
const options: ApexOptions = {
|
||||
colors: ["#5750F1", "#0ABEF9"],
|
||||
chart: {
|
||||
type: "bar",
|
||||
stacked: true,
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1536,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 3,
|
||||
columnWidth: "25%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
borderRadius: 3,
|
||||
columnWidth: "25%",
|
||||
borderRadiusApplication: "end",
|
||||
borderRadiusWhenStacked: "last",
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
grid: {
|
||||
strokeDashArray: 5,
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
xaxis: {
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: "top",
|
||||
horizontalAlign: "left",
|
||||
fontFamily: "inherit",
|
||||
fontWeight: 500,
|
||||
fontSize: "14px",
|
||||
markers: {
|
||||
size: 9,
|
||||
shape: "circle",
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div className="-ml-3.5 mt-3">
|
||||
<Chart
|
||||
options={options}
|
||||
series={[
|
||||
{
|
||||
name: "Sales",
|
||||
data: data.sales,
|
||||
},
|
||||
{
|
||||
name: "Revenue",
|
||||
data: data.revenue,
|
||||
},
|
||||
]}
|
||||
type="bar"
|
||||
height={370}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PeriodPicker } from "@/components/period-picker";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getWeeksProfitData } from "@/services/charts.services";
|
||||
import { WeeksProfitChart } from "./chart";
|
||||
|
||||
type PropsType = {
|
||||
timeFrame?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export async function WeeksProfit({ className, timeFrame }: PropsType) {
|
||||
const data = await getWeeksProfitData(timeFrame);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-[10px] bg-white px-7.5 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 className="text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Profit {timeFrame || "this week"}
|
||||
</h2>
|
||||
|
||||
<PeriodPicker
|
||||
items={["this week", "last week"]}
|
||||
defaultValue={timeFrame || "this week"}
|
||||
sectionKey="weeks_profit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<WeeksProfitChart data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const CheckboxFive = () => {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="checkboxLabelFive"
|
||||
className="flex cursor-pointer select-none items-center text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxLabelFive"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setIsChecked(!isChecked);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`box mr-2 flex h-5 w-5 items-center justify-center rounded-full border border-primary ${
|
||||
isChecked && "!border-4"
|
||||
}`}
|
||||
>
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-white dark:bg-transparent"></span>
|
||||
</div>
|
||||
</div>
|
||||
Checkbox Text
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxFive;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const CheckboxFour = () => {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="checkboxLabelFour"
|
||||
className="flex cursor-pointer select-none items-center text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxLabelFour"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setIsChecked(!isChecked);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`mr-2 flex h-5 w-5 items-center justify-center rounded-full border ${
|
||||
isChecked
|
||||
? "border-primary bg-gray-2 dark:bg-dark-2"
|
||||
: "border-dark-5 dark:border-dark-6"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-2.5 w-2.5 rounded-full bg-transparent ${
|
||||
isChecked && "!bg-primary"
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Checkbox Text
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxFour;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const CheckboxOne = () => {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="checkboxLabelOne"
|
||||
className="flex cursor-pointer select-none items-center text-body-sm font-medium"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxLabelOne"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setIsChecked(!isChecked);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`mr-2 flex h-5 w-5 items-center justify-center rounded border ${
|
||||
isChecked
|
||||
? "border-primary bg-gray-2 dark:bg-transparent"
|
||||
: "border-dark-5 dark:border-dark-6"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-2.5 w-2.5 rounded-sm ${isChecked && "bg-primary"}`}
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
Checkbox Text
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxOne;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const CheckboxThree = () => {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="checkboxLabelThree"
|
||||
className="flex cursor-pointer select-none items-center text-body-sm font-medium"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxLabelThree"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setIsChecked(!isChecked);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`box mr-2 flex h-5 w-5 items-center justify-center rounded border ${
|
||||
isChecked
|
||||
? "border-primary bg-gray-2 dark:bg-transparent"
|
||||
: "border-dark-5 dark:border-dark-6"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-primary opacity-0 ${
|
||||
isChecked && "!opacity-100"
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_803_2686)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.23529 2.29669C0.942402 2.00379 0.942402 1.52892 1.23529 1.23603C1.52819 0.943134 2.00306 0.943134 2.29596 1.23603L5.37433 4.3144L8.45261 1.23612C8.7455 0.943225 9.22038 0.943225 9.51327 1.23612C9.80616 1.52901 9.80616 2.00389 9.51327 2.29678L6.43499 5.37506L9.51327 8.45334C9.80616 8.74624 9.80616 9.22111 9.51327 9.514C9.22038 9.8069 8.7455 9.8069 8.45261 9.514L5.37433 6.43572L2.29596 9.51409C2.00306 9.80699 1.52819 9.80699 1.23529 9.51409C0.942402 9.2212 0.942402 8.74633 1.23529 8.45343L4.31367 5.37506L1.23529 2.29669Z"
|
||||
fill=""
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_803_2686">
|
||||
<rect width="10.75" height="10.75" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Checkbox Text
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxThree;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const CheckboxTwo = () => {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="checkboxLabelTwo"
|
||||
className="flex cursor-pointer select-none items-center text-body-sm font-medium"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkboxLabelTwo"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setIsChecked(!isChecked);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`mr-2 flex h-5 w-5 items-center justify-center rounded border ${
|
||||
isChecked
|
||||
? "border-primary bg-gray-2 dark:bg-transparent"
|
||||
: "border border-dark-5 dark:border-dark-6"
|
||||
}`}
|
||||
>
|
||||
<span className={`opacity-0 ${isChecked && "!opacity-100"}`}>
|
||||
<svg
|
||||
width="11"
|
||||
height="8"
|
||||
viewBox="0 0 11 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.0915 0.951972L10.0867 0.946075L10.0813 0.940568C9.90076 0.753564 9.61034 0.753146 9.42927 0.939309L4.16201 6.22962L1.58507 3.63469C1.40401 3.44841 1.11351 3.44879 0.932892 3.63584C0.755703 3.81933 0.755703 4.10875 0.932892 4.29224L0.932878 4.29225L0.934851 4.29424L3.58046 6.95832C3.73676 7.11955 3.94983 7.2 4.1473 7.2C4.36196 7.2 4.55963 7.11773 4.71406 6.9584L10.0468 1.60234C10.2436 1.4199 10.2421 1.1339 10.0915 0.951972ZM4.2327 6.30081L4.2317 6.2998C4.23206 6.30015 4.23237 6.30049 4.23269 6.30082L4.2327 6.30081Z"
|
||||
fill="#5750F1"
|
||||
stroke="#5750F1"
|
||||
strokeWidth="0.4"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Checkbox Text
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxTwo;
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { Calendar } from "@/components/Layouts/sidebar/icons";
|
||||
import flatpickr from "flatpickr";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const DatePickerOne = () => {
|
||||
useEffect(() => {
|
||||
// Init flatpickr
|
||||
flatpickr(".form-datepicker", {
|
||||
mode: "single",
|
||||
static: true,
|
||||
monthSelectorType: "static",
|
||||
dateFormat: "M j, Y",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="mb-3 block text-body-sm font-medium text-dark dark:text-white">
|
||||
Date picker
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="form-datepicker w-full rounded-[7px] border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal outline-none transition focus:border-primary active:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary"
|
||||
placeholder="mm/dd/yyyy"
|
||||
data-class="flatpickr-right"
|
||||
/>
|
||||
|
||||
<div className="pointer-events-none absolute inset-0 left-auto right-5 flex items-center">
|
||||
<Calendar className="size-5 text-[#9CA3AF]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePickerOne;
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronUpIcon } from "@/assets/icons";
|
||||
import flatpickr from "flatpickr";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const DatePickerTwo = () => {
|
||||
useEffect(() => {
|
||||
// Init flatpickr
|
||||
flatpickr(".form-datepicker", {
|
||||
mode: "single",
|
||||
static: true,
|
||||
monthSelectorType: "static",
|
||||
dateFormat: "M j, Y",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="mb-3 block text-body-sm font-medium text-dark dark:text-white">
|
||||
Select date
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="form-datepicker w-full rounded-[7px] border-[1.5px] border-stroke bg-transparent px-5 py-3 font-normal outline-none transition focus:border-primary active:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary"
|
||||
placeholder="mm/dd/yyyy"
|
||||
data-class="flatpickr-right"
|
||||
/>
|
||||
|
||||
<div className="pointer-events-none absolute inset-0 left-auto right-5 flex items-center text-dark-4 dark:text-dark-6">
|
||||
<ChevronUpIcon className="rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePickerTwo;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type HTMLInputTypeAttribute, useId } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
type Path,
|
||||
type UseFormRegister,
|
||||
type UseFormRegisterReturn,
|
||||
} from "react-hook-form";
|
||||
|
||||
type InputGroupProps<T extends FieldValues> = {
|
||||
name: Path<T>;
|
||||
label: string;
|
||||
type: HTMLInputTypeAttribute;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
fileStyleVariant?: "style1" | "style2";
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
handleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value?: string;
|
||||
icon?: React.ReactNode;
|
||||
iconPosition?: "left" | "right";
|
||||
height?: "sm" | "default";
|
||||
defaultValue?: string;
|
||||
register?: UseFormRegister<T>;
|
||||
errors?: FieldErrors<T>;
|
||||
} & Partial<UseFormRegisterReturn>;
|
||||
|
||||
const InputGroup = <T extends FieldValues>({
|
||||
className,
|
||||
label,
|
||||
type,
|
||||
placeholder,
|
||||
required,
|
||||
disabled,
|
||||
active,
|
||||
handleChange,
|
||||
icon,
|
||||
name,
|
||||
register,
|
||||
errors,
|
||||
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
...props
|
||||
}: InputGroupProps<T>): React.ReactElement => {
|
||||
const id = useId();
|
||||
|
||||
const error = errors && errors[name];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red ml-1 select-none">*</span>}
|
||||
</label>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative mt-3 [&_svg]:absolute [&_svg]:top-1/2 [&_svg]:-translate-y-1/2",
|
||||
props.iconPosition === "left"
|
||||
? "[&_svg]:left-4.5"
|
||||
: "[&_svg]:right-4.5",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
ref={ref}
|
||||
onChange={(e) => {
|
||||
onChange?.(e);
|
||||
handleChange?.(e);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
{...(!onChange &&
|
||||
props.value !== undefined && { value: props.value })}
|
||||
{...(!onChange &&
|
||||
props.defaultValue !== undefined && {
|
||||
defaultValue: props.defaultValue,
|
||||
})}
|
||||
className={cn(
|
||||
"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"
|
||||
? getFileStyles(props.fileStyleVariant!)
|
||||
: "px-5.5 py-3 text-dark placeholder:text-dark-6 dark:text-white",
|
||||
props.iconPosition === "left" && "pl-12.5",
|
||||
props.height === "sm" && "py-2.5",
|
||||
)}
|
||||
disabled={disabled}
|
||||
data-active={active}
|
||||
/>
|
||||
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputGroup;
|
||||
|
||||
function getFileStyles(variant: "style1" | "style2") {
|
||||
switch (variant) {
|
||||
case "style1":
|
||||
return `file:mr-5 file:border-collapse file:cursor-pointer file:border-0 file:border-r file:border-solid file:border-stroke file:bg-[#E2E8F0] file:px-6.5 file:py-[13px] file:text-body-sm file:font-medium file:text-dark-5 file:hover:bg-primary file:hover:bg-opacity-10 dark:file:border-dark-3 dark:file:bg-white/30 dark:file:text-white`;
|
||||
default:
|
||||
return `file:mr-4 file:rounded file:border-[0.5px] file:border-stroke file:bg-stroke file:px-2.5 file:py-1 file:text-body-xs file:font-medium file:text-dark-5 file:focus:border-primary dark:file:border-dark-3 dark:file:bg-white/30 dark:file:text-white px-3 py-[9px]`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useId } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
type Path,
|
||||
type UseFormRegister,
|
||||
type UseFormRegisterReturn,
|
||||
} from "react-hook-form";
|
||||
|
||||
type PropsType<T extends FieldValues> = {
|
||||
name: Path<T>;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
defaultValue?: string;
|
||||
handleChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
register?: UseFormRegister<T>;
|
||||
errors?: FieldErrors<T>;
|
||||
} & Partial<UseFormRegisterReturn>;
|
||||
|
||||
export function TextAreaGroup<T extends FieldValues>({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
required,
|
||||
disabled,
|
||||
active,
|
||||
className,
|
||||
icon,
|
||||
defaultValue,
|
||||
handleChange,
|
||||
register,
|
||||
errors,
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
...props
|
||||
}: PropsType<T>) {
|
||||
const id = useId();
|
||||
|
||||
const error = errors && errors[name];
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="mb-3 block text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red ml-1 select-none">*</span>}
|
||||
</label>
|
||||
|
||||
<div className="relative mt-3 [&_svg]:pointer-events-none [&_svg]:absolute [&_svg]:left-5.5 [&_svg]:top-5.5">
|
||||
<textarea
|
||||
id={id}
|
||||
name={name}
|
||||
ref={ref}
|
||||
rows={6}
|
||||
placeholder={placeholder}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => {
|
||||
onChange?.(e);
|
||||
handleChange?.(e);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
className={cn(
|
||||
"w-full rounded-lg border-[1.5px] border-stroke bg-transparent px-5.5 py-3 text-dark 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:text-white dark:focus:border-primary dark:disabled:bg-dark dark:data-[active=true]:border-primary",
|
||||
error && "border-red focus:border-red dark:border-red",
|
||||
icon && "py-5 pl-13 pr-5",
|
||||
)}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
data-active={active}
|
||||
/>
|
||||
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useId } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
type Path,
|
||||
type UseFormRegisterReturn,
|
||||
} from "react-hook-form";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
text: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
type MultiSelectProps<T extends FieldValues> = {
|
||||
label: string;
|
||||
items: { value: string; text: string }[];
|
||||
errors?: FieldErrors<T>;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
} & UseFormRegisterReturn<Path<T>>;
|
||||
|
||||
function MultiSelect<T extends FieldValues>({
|
||||
name,
|
||||
label,
|
||||
items,
|
||||
errors,
|
||||
required,
|
||||
placeholder = "Select an option",
|
||||
className,
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
}: MultiSelectProps<T>) {
|
||||
const id = useId();
|
||||
const error = errors && errors[name];
|
||||
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const [show, setShow] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const trigger = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
selected: false,
|
||||
})),
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const open = () => {
|
||||
setShow(true);
|
||||
};
|
||||
|
||||
const isOpen = () => {
|
||||
return show === true;
|
||||
};
|
||||
|
||||
const combinedRef = (el: HTMLDivElement) => {
|
||||
trigger.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (index: number) => {
|
||||
const newOptions = [...options];
|
||||
let newSelected = [...selected];
|
||||
|
||||
if (!newOptions[index].selected) {
|
||||
newOptions[index].selected = true;
|
||||
newSelected.push(index);
|
||||
} else {
|
||||
newOptions[index].selected = false;
|
||||
newSelected = newSelected.filter((i) => i !== index);
|
||||
}
|
||||
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => options[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
};
|
||||
|
||||
const remove = (index: number, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const newOptions = [...options];
|
||||
const newSelected = selected.filter((i) => i !== index);
|
||||
newOptions[index].selected = false;
|
||||
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => options[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const clickHandler = ({ target }: MouseEvent) => {
|
||||
if (!dropdownRef.current) return;
|
||||
if (
|
||||
!show ||
|
||||
dropdownRef.current.contains(target as Node) ||
|
||||
trigger.current?.contains(target as Node)
|
||||
)
|
||||
return;
|
||||
setShow(false);
|
||||
};
|
||||
document.addEventListener("click", clickHandler);
|
||||
return () => document.removeEventListener("click", clickHandler);
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<div className={`relative z-50 ${className}`}>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="mb-3 block text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={combinedRef}
|
||||
onClick={open}
|
||||
onBlur={onBlur}
|
||||
tabIndex={0}
|
||||
className={`flex min-h-[46px] w-full cursor-pointer items-center rounded-[7px] border-[1.5px] bg-transparent py-2 pl-3 pr-3 outline-none transition focus:border-primary active:border-primary dark:bg-dark-2 ${
|
||||
error
|
||||
? "border-red focus:border-red dark:border-red"
|
||||
: "border-stroke dark:border-dark-3"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-auto flex-wrap gap-2">
|
||||
{selected.map((index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center rounded-[5px] border-[.5px] border-stroke bg-gray-2 px-2.5 py-1 text-body-sm font-medium dark:border-dark-3 dark:bg-dark"
|
||||
>
|
||||
<div className="max-w-full flex-initial">
|
||||
{options[index]?.text}
|
||||
</div>
|
||||
<div className="flex flex-auto flex-row-reverse">
|
||||
<div
|
||||
onClick={(e) => remove(index, e)}
|
||||
className="hover:text-red cursor-pointer pl-2"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
role="button"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{selected.length === 0 && (
|
||||
<div className="flex-1">
|
||||
<span className="h-full w-full bg-transparent p-1 px-2 text-dark-5 outline-none dark:text-dark-6">
|
||||
{placeholder}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center py-1 pl-1 pr-1">
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-dark-4 outline-none focus:outline-none dark:text-dark-6"
|
||||
>
|
||||
<svg
|
||||
className={`fill-current transition-transform duration-200 ${
|
||||
isOpen() ? "rotate-180" : ""
|
||||
}`}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.69149 7.09327C3.91613 6.83119 4.31069 6.80084 4.57277 7.02548L9.99936 11.6768L15.4259 7.02548C15.688 6.80084 16.0826 6.83119 16.3072 7.09327C16.5319 7.35535 16.5015 7.74991 16.2394 7.97455L10.4061 12.9745C10.172 13.1752 9.82667 13.1752 9.59261 12.9745L3.75928 7.97455C3.4972 7.74991 3.46685 7.35535 3.69149 7.09327Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`max-h-select absolute left-0 top-full z-40 w-full overflow-y-auto rounded bg-white shadow-1 dark:bg-dark-2 dark:shadow-card ${
|
||||
isOpen() ? "" : "hidden"
|
||||
}`}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
{options.map((option, index) => (
|
||||
<div key={index}>
|
||||
<div
|
||||
className="w-full cursor-pointer rounded-t border-b border-stroke hover:bg-primary/5 dark:border-dark-3"
|
||||
onClick={() => handleSelect(index)}
|
||||
>
|
||||
<div
|
||||
className={`relative flex w-full items-center border-l-2 p-2 pl-2 ${
|
||||
option.selected ? "border-primary" : "border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="mx-2 leading-6">{option.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default MultiSelect;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const SwitcherFour = () => {
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="toggle4"
|
||||
className="flex cursor-pointer select-none items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle4"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
/>
|
||||
<div className="block h-8 w-14 rounded-full bg-[#212B36] dark:bg-primary"></div>
|
||||
<div
|
||||
className={`absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition ${
|
||||
enabled && "!right-1 !translate-x-full"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitcherFour;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const SwitcherOne = () => {
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="toggle1"
|
||||
className="flex cursor-pointer select-none items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle1"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
/>
|
||||
<div className="block h-8 w-14 rounded-full bg-gray-3 dark:bg-[#5A616B]"></div>
|
||||
<div
|
||||
className={`absolute left-1 top-1 h-6 w-6 rounded-full bg-white shadow-switch-1 transition ${
|
||||
enabled && "!right-1 !translate-x-full !bg-primary dark:!bg-white"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitcherOne;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const SwitcherThree = () => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="toggle3"
|
||||
className="flex cursor-pointer select-none items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle3"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
/>
|
||||
<div className="block h-8 w-14 rounded-full bg-gray-3 dark:bg-[#5A616B]"></div>
|
||||
<div
|
||||
className={`dot absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white shadow-switch-1 transition ${
|
||||
enabled && "!right-1 !translate-x-full !bg-primary dark:!bg-white"
|
||||
}`}
|
||||
>
|
||||
<span className={`hidden ${enabled && "!block"}`}>
|
||||
<svg
|
||||
className="fill-white dark:fill-dark"
|
||||
width="11"
|
||||
height="8"
|
||||
viewBox="0 0 11 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.2355 0.812752L10.2452 0.824547C10.4585 1.08224 10.4617 1.48728 10.1855 1.74621L4.85633 7.09869C4.66442 7.29617 4.41535 7.4001 4.14693 7.4001C3.89823 7.4001 3.63296 7.29979 3.43735 7.09851L0.788615 4.43129C0.536589 4.1703 0.536617 3.758 0.788643 3.49701C1.04747 3.22897 1.4675 3.22816 1.72731 3.49457L4.16182 5.94608L9.28643 0.799032C9.54626 0.532887 9.96609 0.533789 10.2248 0.801737L10.2355 0.812752Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span className={`${enabled && "hidden"}`}>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_803_2686)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.23529 2.29669C0.942402 2.00379 0.942402 1.52892 1.23529 1.23603C1.52819 0.943134 2.00306 0.943134 2.29596 1.23603L5.37433 4.3144L8.45261 1.23612C8.7455 0.943225 9.22038 0.943225 9.51327 1.23612C9.80616 1.52901 9.80616 2.00389 9.51327 2.29678L6.43499 5.37506L9.51327 8.45334C9.80616 8.74624 9.80616 9.22111 9.51327 9.514C9.22038 9.8069 8.7455 9.8069 8.45261 9.514L5.37433 6.43572L2.29596 9.51409C2.00306 9.80699 1.52819 9.80699 1.23529 9.51409C0.942402 9.2212 0.942402 8.74633 1.23529 8.45343L4.31367 5.37506L1.23529 2.29669Z"
|
||||
fill=""
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_803_2686">
|
||||
<rect width="10.75" height="10.75" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitcherThree;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const SwitcherTwo = () => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="toggle2"
|
||||
className="flex cursor-pointer select-none items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="toggle2"
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
onChange={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
/>
|
||||
<div className="h-5 w-14 rounded-full bg-gray-3 dark:bg-[#5A616B]"></div>
|
||||
<div
|
||||
className={`dot shadow-switch-2 absolute -top-1 left-0 h-7 w-7 rounded-full bg-white transition ${
|
||||
enabled && "!right-0 !translate-x-full !bg-primary dark:!bg-white"
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitcherTwo;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { CheckIcon, XIcon } from "@/assets/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useId } from "react";
|
||||
|
||||
type PropsType = {
|
||||
withIcon?: "check" | "x";
|
||||
withBg?: boolean;
|
||||
label: string;
|
||||
name?: string;
|
||||
minimal?: boolean;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
radius?: "default" | "md";
|
||||
};
|
||||
|
||||
export function Checkbox({
|
||||
withIcon,
|
||||
label,
|
||||
name,
|
||||
withBg,
|
||||
minimal,
|
||||
onChange,
|
||||
radius,
|
||||
}: PropsType) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
"flex cursor-pointer select-none items-center",
|
||||
!minimal && "text-body-sm font-medium",
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={onChange}
|
||||
name={name}
|
||||
id={id}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex size-5 items-center justify-center rounded border border-dark-5 peer-checked:border-primary dark:border-dark-6 peer-checked:[&>*]:block",
|
||||
withBg
|
||||
? "peer-checked:bg-primary [&>*]:text-white"
|
||||
: "peer-checked:bg-gray-2 dark:peer-checked:bg-transparent",
|
||||
minimal && "mr-3 border-stroke dark:border-dark-3",
|
||||
radius === "md" && "rounded-md",
|
||||
)}
|
||||
>
|
||||
{!withIcon && (
|
||||
<span className="hidden size-2.5 rounded-sm bg-primary" />
|
||||
)}
|
||||
|
||||
{withIcon === "check" && (
|
||||
<CheckIcon className="hidden text-primary" />
|
||||
)}
|
||||
|
||||
{withIcon === "x" && <XIcon className="hidden text-primary" />}
|
||||
</div>
|
||||
</div>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useId } from "react";
|
||||
|
||||
type PropsType = {
|
||||
variant?: "dot" | "circle";
|
||||
label: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
minimal?: boolean;
|
||||
};
|
||||
|
||||
export function RadioInput({
|
||||
label,
|
||||
variant = "dot",
|
||||
name,
|
||||
value,
|
||||
minimal,
|
||||
}: PropsType) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="flex cursor-pointer select-none items-center text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
id={id}
|
||||
className="peer sr-only"
|
||||
value={value}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex size-5 items-center justify-center rounded-full border peer-checked:[&>*]:block",
|
||||
{
|
||||
"border-primary peer-checked:border-6": variant === "circle",
|
||||
"border-dark-5 peer-checked:border-primary peer-checked:bg-gray-2 dark:border-dark-6 dark:peer-checked:bg-dark-2":
|
||||
variant === "dot",
|
||||
},
|
||||
minimal && "border-stroke dark:border-dark-3",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"hidden size-2.5 rounded-full bg-primary",
|
||||
variant === "circle" && "bg-transparent",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronUpIcon } from "@/assets/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useId, useState } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
type Path,
|
||||
type UseFormRegister,
|
||||
type UseFormRegisterReturn,
|
||||
} from "react-hook-form";
|
||||
|
||||
type PropsType<T extends FieldValues> = {
|
||||
name: Path<T>;
|
||||
label: string;
|
||||
items: { value: string; label: string }[];
|
||||
prefixIcon?: React.ReactNode;
|
||||
className?: string;
|
||||
register?: UseFormRegister<T>;
|
||||
errors?: FieldErrors<T>;
|
||||
required?: boolean;
|
||||
} & (
|
||||
| { placeholder?: string; defaultValue: string }
|
||||
| { placeholder: string; defaultValue?: string }
|
||||
) &
|
||||
Partial<UseFormRegisterReturn>;
|
||||
|
||||
export function Select<T extends FieldValues>({
|
||||
items,
|
||||
label,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
prefixIcon,
|
||||
className,
|
||||
name,
|
||||
register,
|
||||
errors,
|
||||
required,
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
...props
|
||||
}: PropsType<T>) {
|
||||
const id = useId();
|
||||
|
||||
const [isOptionSelected, setIsOptionSelected] = useState(!!defaultValue);
|
||||
const error = errors && errors[name];
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3", className)}>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="block text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red ml-1 select-none">*</span>}
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
{prefixIcon && (
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2">
|
||||
{prefixIcon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<select
|
||||
id={id}
|
||||
name={name}
|
||||
ref={ref}
|
||||
defaultValue={defaultValue || ""}
|
||||
onChange={(e) => {
|
||||
setIsOptionSelected(!!e.target.value);
|
||||
onChange?.(e);
|
||||
}}
|
||||
onBlur={onBlur}
|
||||
className={cn(
|
||||
"w-full appearance-none rounded-lg border border-stroke bg-transparent px-5.5 py-3 outline-none transition focus:border-primary active:border-primary dark:border-dark-3 dark:bg-dark-2 dark:focus:border-primary [&>option]:text-dark-5 dark:[&>option]:text-dark-6",
|
||||
isOptionSelected && "text-dark dark:text-white",
|
||||
error && "border-red focus:border-red dark:border-red",
|
||||
prefixIcon && "pl-11.5",
|
||||
)}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled hidden>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
|
||||
{items.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<ChevronUpIcon className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 rotate-180" />
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CheckIcon, XIcon } from "@/assets/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useId } from "react";
|
||||
|
||||
type PropsType = {
|
||||
withIcon?: boolean;
|
||||
background?: "dark" | "light";
|
||||
backgroundSize?: "sm" | "default";
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function Switch({
|
||||
background,
|
||||
withIcon,
|
||||
backgroundSize,
|
||||
name,
|
||||
}: PropsType) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="flex max-w-fit cursor-pointer select-none items-center"
|
||||
>
|
||||
<div className="relative">
|
||||
<input type="checkbox" name={name} id={id} className="peer sr-only" />
|
||||
<div
|
||||
className={cn("h-8 w-14 rounded-full bg-gray-3 dark:bg-[#5A616B]", {
|
||||
"h-5": backgroundSize === "sm",
|
||||
"bg-[#212B36] dark:bg-primary": background === "dark",
|
||||
})}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-1 top-1 flex size-6 items-center justify-center rounded-full bg-white shadow-switch-1 transition peer-checked:right-1 peer-checked:translate-x-full peer-checked:[&_.check-icon]:block peer-checked:[&_.x-icon]:hidden",
|
||||
{
|
||||
"-top-1 left-0 size-7 shadow-switch-2": backgroundSize === "sm",
|
||||
"peer-checked:bg-primary peer-checked:dark:bg-white":
|
||||
background !== "dark",
|
||||
},
|
||||
)}
|
||||
>
|
||||
{withIcon && (
|
||||
<>
|
||||
<CheckIcon className="check-icon hidden fill-white dark:fill-dark" />
|
||||
<XIcon className="x-icon" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Header } from "./header";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { AuthGuard, LoginGuard } from "@/contexts";
|
||||
|
||||
export function ConditionalLayout({ children }: PropsWithChildren) {
|
||||
const pathname = usePathname();
|
||||
const isAuthPage = pathname.startsWith("/auth");
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthPage ? (
|
||||
<LoginGuard>
|
||||
<main>{children}</main>
|
||||
</LoginGuard>
|
||||
) : (
|
||||
<AuthGuard>
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<div className="w-full bg-gray-2 dark:bg-[#020d1a]">
|
||||
<Header />
|
||||
<main className="isolate mx-auto w-full max-w-screen-2xl overflow-hidden p-4 md:p-6 2xl:p-10">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AuthGuard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { IconProps } from "@/types/icon-props";
|
||||
|
||||
export function MenuIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M3.5625 6C3.5625 5.58579 3.89829 5.25 4.3125 5.25H20.3125C20.7267 5.25 21.0625 5.58579 21.0625 6C21.0625 6.41421 20.7267 6.75 20.3125 6.75L4.3125 6.75C3.89829 6.75 3.5625 6.41422 3.5625 6Z" />
|
||||
<path d="M3.5625 18C3.5625 17.5858 3.89829 17.25 4.3125 17.25L20.3125 17.25C20.7267 17.25 21.0625 17.5858 21.0625 18C21.0625 18.4142 20.7267 18.75 20.3125 18.75L4.3125 18.75C3.89829 18.75 3.5625 18.4142 3.5625 18Z" />
|
||||
<path d="M4.3125 11.25C3.89829 11.25 3.5625 11.5858 3.5625 12C3.5625 12.4142 3.89829 12.75 4.3125 12.75L20.3125 12.75C20.7267 12.75 21.0625 12.4142 21.0625 12C21.0625 11.5858 20.7267 11.25 20.3125 11.25L4.3125 11.25Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useSidebarContext } from "../sidebar/sidebar-context";
|
||||
import { MenuIcon } from "./icons";
|
||||
import { Notification } from "./notification";
|
||||
import { ThemeToggleSwitch } from "./theme-toggle";
|
||||
import { UserInfo } from "./user-info";
|
||||
|
||||
export function Header() {
|
||||
const { toggleSidebar, isMobile } = useSidebarContext();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 flex 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">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="rounded-lg border px-1.5 py-1 dark:border-stroke-dark dark:bg-[#020D1A] hover:dark:bg-[#FFFFFF1A] lg:hidden"
|
||||
>
|
||||
<MenuIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</button>
|
||||
|
||||
{isMobile && (
|
||||
<Link href={"/"} className="ml-2 max-[430px]:hidden min-[375px]:ml-4">
|
||||
<Image
|
||||
src={"/images/logo/logo-icon.svg"}
|
||||
width={32}
|
||||
height={32}
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div className="max-xl:hidden">
|
||||
<h1 className="mb-0.5 text-heading-5 font-bold 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">
|
||||
<ThemeToggleSwitch />
|
||||
|
||||
<Notification />
|
||||
|
||||
<div className="shrink-0">
|
||||
<UserInfo />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function BellIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width={20} height={20} viewBox="0 0 20 20" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 1.042A6.458 6.458 0 003.542 7.5v.587c0 .58-.172 1.148-.495 1.631l-.957 1.436a2.934 2.934 0 001.67 4.459c.63.171 1.264.316 1.903.435l.002.005c.64 1.71 2.353 2.905 4.335 2.905 1.982 0 3.694-1.196 4.335-2.905l.002-.005a23.736 23.736 0 001.903-.435 2.934 2.934 0 001.67-4.459l-.958-1.436a2.941 2.941 0 01-.494-1.631V7.5A6.458 6.458 0 0010 1.042zm2.813 15.239a23.71 23.71 0 01-5.627 0c.593.85 1.623 1.427 2.814 1.427 1.19 0 2.221-.576 2.813-1.427zM4.792 7.5a5.208 5.208 0 1110.416 0v.587c0 .827.245 1.636.704 2.325l.957 1.435c.638.957.151 2.257-.958 2.56a22.467 22.467 0 01-11.822 0 1.684 1.684 0 01-.959-2.56l.958-1.435a4.192 4.192 0 00.704-2.325V7.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownContent,
|
||||
DropdownTrigger,
|
||||
} from "@/components/ui/dropdown";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { BellIcon } from "./icons";
|
||||
|
||||
const notificationList = [
|
||||
{
|
||||
image: "/images/user/user-15.png",
|
||||
title: "Piter Joined the Team!",
|
||||
subTitle: "Congratulate him",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-03.png",
|
||||
title: "New message",
|
||||
subTitle: "Devid sent a new message",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-26.png",
|
||||
title: "New Payment received",
|
||||
subTitle: "Check your earnings",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-28.png",
|
||||
title: "Jolly completed tasks",
|
||||
subTitle: "Assign new task",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-27.png",
|
||||
title: "Roman Joined the Team!",
|
||||
subTitle: "Congratulate him",
|
||||
},
|
||||
];
|
||||
|
||||
export function Notification() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDotVisible, setIsDotVisible] = useState(true);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
setIsOpen={(open) => {
|
||||
setIsOpen(open);
|
||||
|
||||
if (setIsDotVisible) setIsDotVisible(false);
|
||||
}}
|
||||
>
|
||||
<DropdownTrigger
|
||||
className="grid size-12 place-items-center rounded-full border bg-gray-2 text-dark outline-none hover:text-primary focus-visible:border-primary focus-visible:text-primary dark:border-dark-4 dark:bg-dark-3 dark:text-white dark:focus-visible:border-primary"
|
||||
aria-label="View Notifications"
|
||||
>
|
||||
<span className="relative">
|
||||
<BellIcon />
|
||||
|
||||
{isDotVisible && (
|
||||
<span
|
||||
className={cn(
|
||||
"absolute right-0 top-0 z-1 size-2 rounded-full bg-red-light ring-2 ring-gray-2 dark:ring-dark-3",
|
||||
)}
|
||||
>
|
||||
<span className="absolute inset-0 -z-1 animate-ping rounded-full bg-red-light opacity-75" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent
|
||||
align={isMobile ? "end" : "center"}
|
||||
className="border border-stroke bg-white px-3.5 py-3 shadow-md dark:border-dark-3 dark:bg-gray-dark min-[350px]:min-w-[20rem]"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between px-2 py-1.5">
|
||||
<span className="text-lg font-medium text-dark dark:text-white">
|
||||
Notifications
|
||||
</span>
|
||||
<span className="rounded-md bg-primary px-[9px] py-0.5 text-xs font-medium text-white">
|
||||
5 new
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="mb-3 max-h-[23rem] space-y-1.5 overflow-y-auto">
|
||||
{notificationList.map((item, index) => (
|
||||
<li key={index} role="menuitem">
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-4 rounded-lg px-2 py-1.5 outline-none hover:bg-gray-2 focus-visible:bg-gray-2 dark:hover:bg-dark-3 dark:focus-visible:bg-dark-3"
|
||||
>
|
||||
<Image
|
||||
src={item.image}
|
||||
className="size-14 rounded-full object-cover"
|
||||
width={200}
|
||||
height={200}
|
||||
alt="User"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<strong className="block text-sm font-medium text-dark dark:text-white">
|
||||
{item.title}
|
||||
</strong>
|
||||
|
||||
<span className="truncate text-sm font-medium text-dark-5 dark:text-dark-6">
|
||||
{item.subTitle}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="block rounded-lg border border-primary p-2 text-center text-sm font-medium tracking-wide text-primary outline-none transition-colors hover:bg-blue-light-5 focus:bg-blue-light-5 focus:text-primary focus-visible:border-primary dark:border-dark-3 dark:text-dark-6 dark:hover:border-dark-5 dark:hover:bg-dark-3 dark:hover:text-dark-7 dark:focus-visible:border-dark-5 dark:focus-visible:bg-dark-3 dark:focus-visible:text-dark-7"
|
||||
>
|
||||
See all notifications
|
||||
</Link>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function Sun(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10 1.042c.345 0 .625.28.625.625V2.5a.625.625 0 11-1.25 0v-.833c0-.346.28-.625.625-.625zM3.666 3.665a.625.625 0 01.883 0l.328.328a.625.625 0 01-.884.884l-.327-.328a.625.625 0 010-.884zm12.668 0a.625.625 0 010 .884l-.327.328a.625.625 0 01-.884-.884l.327-.327a.625.625 0 01.884 0zM10 5.626a4.375 4.375 0 100 8.75 4.375 4.375 0 000-8.75zM4.375 10a5.625 5.625 0 1111.25 0 5.625 5.625 0 01-11.25 0zm-3.333 0c0-.345.28-.625.625-.625H2.5a.625.625 0 110 1.25h-.833A.625.625 0 011.042 10zm15.833 0c0-.345.28-.625.625-.625h.833a.625.625 0 010 1.25H17.5a.625.625 0 01-.625-.625zm-1.752 5.123a.625.625 0 01.884 0l.327.327a.625.625 0 11-.884.884l-.327-.327a.625.625 0 010-.884zm-10.246 0a.625.625 0 010 .884l-.328.327a.625.625 0 11-.883-.884l.327-.327a.625.625 0 01.884 0zM10 16.875c.345 0 .625.28.625.625v.833a.625.625 0 01-1.25 0V17.5c0-.345.28-.625.625-.625z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Moon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.18 2.334a7.71 7.71 0 108.485 8.485A6.042 6.042 0 119.18 2.335zM1.042 10a8.958 8.958 0 018.958-8.958c.598 0 .896.476.948.855.049.364-.086.828-.505 1.082a4.792 4.792 0 106.579 6.579c.253-.42.717-.555 1.081-.506.38.052.856.35.856.948A8.958 8.958 0 011.04 10z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Moon, Sun } from "./icons";
|
||||
|
||||
const THEMES = [
|
||||
{
|
||||
name: "light",
|
||||
Icon: Sun,
|
||||
},
|
||||
{
|
||||
name: "dark",
|
||||
Icon: Moon,
|
||||
},
|
||||
];
|
||||
|
||||
export function ThemeToggleSwitch() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
className="group rounded-full bg-gray-3 p-[5px] text-[#111928] outline-1 outline-primary focus-visible:outline dark:bg-[#020D1A] dark:text-current"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Switch to {theme === "light" ? "dark" : "light"} mode
|
||||
</span>
|
||||
|
||||
<span aria-hidden className="relative flex gap-2.5">
|
||||
{/* Indicator */}
|
||||
<span className="absolute size-[38px] rounded-full border border-gray-200 bg-white transition-all dark:translate-x-[48px] dark:border-none dark:bg-dark-2 dark:group-hover:bg-dark-3" />
|
||||
|
||||
{THEMES.map(({ name, Icon }) => (
|
||||
<span
|
||||
key={name}
|
||||
className={cn(
|
||||
"relative grid size-[38px] place-items-center rounded-full",
|
||||
name === "dark" && "dark:text-white",
|
||||
)}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
type SVGPropsType = SVGProps<SVGSVGElement>;
|
||||
|
||||
export function UserIcon(props: SVGPropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 .938a3.562 3.562 0 100 7.124A3.562 3.562 0 009 .938zM6.562 4.5a2.437 2.437 0 114.875 0 2.437 2.437 0 01-4.875 0zM9 9.188c-1.735 0-3.334.394-4.518 1.06-1.167.657-2.045 1.652-2.045 2.877v.076c0 .872-.001 1.965.958 2.747.472.384 1.132.657 2.025.838.894.181 2.06.276 3.58.276s2.685-.095 3.58-.276c.893-.18 1.553-.454 2.025-.838.96-.782.958-1.875.957-2.747v-.076c0-1.226-.877-2.22-2.044-2.877-1.184-.666-2.783-1.06-4.518-1.06zm-5.438 3.937c0-.639.467-1.331 1.471-1.896.987-.555 2.388-.916 3.967-.916 1.579 0 2.98.36 3.967.916 1.004.565 1.47 1.258 1.47 1.896 0 .98-.03 1.533-.542 1.95-.278.227-.743.448-1.538.609-.793.16-1.876.254-3.357.254-1.48 0-2.564-.094-3.357-.255-.795-.16-1.26-.381-1.538-.608-.512-.417-.543-.97-.543-1.95z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsIcon(props: SVGPropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 6.188a2.813 2.813 0 100 5.625 2.813 2.813 0 000-5.626zM7.312 9a1.688 1.688 0 113.376 0 1.688 1.688 0 01-3.376 0z"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.981.938c-.333 0-.612 0-.84.015a2.117 2.117 0 00-.68.142c-.506.209-.907.61-1.117 1.116-.108.263-.138.54-.15.841a.65.65 0 01-.311.55.65.65 0 01-.631-.005c-.267-.141-.522-.254-.804-.291a2.063 2.063 0 00-1.525.408c-.187.144-.33.32-.462.519-.128.19-.267.43-.434.72l-.019.032c-.166.289-.306.53-.406.735a2.117 2.117 0 00-.218.66c-.071.543.076 1.091.409 1.525.173.226.398.39.654.55A.65.65 0 012.766 9a.65.65 0 01-.32.544c-.255.16-.48.325-.653.55-.333.435-.48.983-.409 1.526.03.233.113.445.218.66.1.205.24.446.406.735l.02.033c.166.288.305.53.433.72.133.197.275.374.462.518.434.333.983.48 1.525.408.282-.037.537-.15.804-.29a.65.65 0 01.63-.005.65.65 0 01.313.549c.011.3.04.578.15.841.209.506.61.907 1.116 1.117.217.09.442.125.68.14.228.017.507.017.84.017h.038c.333 0 .612 0 .84-.016.238-.016.463-.051.68-.142.506-.209.907-.61 1.117-1.116.108-.263.138-.54.15-.841a.65.65 0 01.311-.55.65.65 0 01.631.005c.267.141.522.254.804.291a2.062 2.062 0 001.525-.408c.187-.144.33-.32.462-.519.128-.19.267-.43.434-.72l.019-.032c.166-.289.305-.53.406-.736.105-.214.187-.426.218-.66a2.062 2.062 0 00-.409-1.524c-.173-.226-.398-.39-.654-.55A.65.65 0 0115.234 9a.65.65 0 01.32-.544c.255-.16.48-.325.653-.55.333-.435.48-.983.409-1.526a2.117 2.117 0 00-.218-.66c-.1-.205-.24-.446-.406-.735l-.02-.033c-.166-.288-.305-.53-.433-.72a2.117 2.117 0 00-.462-.518 2.062 2.062 0 00-1.525-.408c-.282.037-.537.15-.804.29a.65.65 0 01-.63.005.65.65 0 01-.313-.549c-.011-.3-.04-.578-.15-.841a2.063 2.063 0 00-1.116-1.116 2.118 2.118 0 00-.68-.142c-.228-.016-.507-.016-.84-.015H8.98zm-1.09 1.196c.058-.024.146-.046.327-.059.185-.012.425-.013.782-.013.357 0 .597 0 .782.013.181.013.269.035.327.059.23.095.412.278.507.507.03.073.055.186.065.453.022.595.329 1.167.874 1.481a1.775 1.775 0 001.719.016c.237-.125.347-.16.425-.17a.938.938 0 01.693.186c.05.038.113.103.214.253.103.155.223.362.402.671.179.31.298.517.38.684.08.163.104.25.113.312a.937.937 0 01-.186.693c-.048.062-.133.14-.36.283A1.775 1.775 0 0014.109 9c0 .629.342 1.18.846 1.497.227.143.312.22.36.283a.938.938 0 01.186.693c-.009.062-.033.15-.113.312-.082.167-.201.374-.38.684-.179.309-.299.516-.402.67-.101.151-.165.216-.214.254a.937.937 0 01-.693.186c-.078-.01-.188-.045-.425-.17a1.775 1.775 0 00-1.72.016 1.775 1.775 0 00-.873 1.48c-.01.268-.035.381-.065.454a.937.937 0 01-.507.507 1.034 1.034 0 01-.327.059c-.185.012-.425.012-.782.012-.357 0-.597 0-.782-.012a1.033 1.033 0 01-.327-.059.937.937 0 01-.507-.507c-.03-.073-.055-.186-.065-.454a1.775 1.775 0 00-.874-1.48 1.775 1.775 0 00-1.719-.016c-.237.125-.347.16-.425.17a.937.937 0 01-.693-.186 1.034 1.034 0 01-.214-.253 12.818 12.818 0 01-.402-.671c-.179-.31-.298-.517-.38-.684a1.035 1.035 0 01-.113-.312.937.937 0 01.186-.693c.048-.063.133-.14.36-.283.504-.316.846-.868.846-1.497 0-.629-.342-1.18-.846-1.497-.227-.143-.312-.22-.36-.283a.937.937 0 01-.186-.693c.009-.062.033-.15.113-.312.082-.167.201-.375.38-.684.179-.31.299-.517.402-.67.101-.151.165-.216.214-.254a.938.938 0 01.693-.186c.078.01.188.045.425.17a1.775 1.775 0 001.72-.016c.544-.314.85-.886.873-1.48.01-.268.035-.381.065-.454a.937.937 0 01.507-.507z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogOutIcon(props: SVGPropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_7095_11691)">
|
||||
<path d="M11.209.938c-1.026 0-1.852 0-2.503.087-.675.09-1.243.285-1.695.736-.393.394-.592.878-.697 1.446-.101.553-.12 1.229-.125 2.04a.562.562 0 101.125.006c.005-.82.026-1.401.107-1.842.078-.426.203-.672.386-.854.207-.208.499-.343 1.05-.417.566-.076 1.317-.078 2.393-.078H12c1.077 0 1.828.002 2.394.078.55.074.842.21 1.05.417.207.207.342.499.416 1.05.077.566.078 1.316.078 2.393v6c0 1.077-.002 1.827-.078 2.394-.074.55-.209.842-.417 1.05-.207.207-.499.342-1.049.416-.566.076-1.317.078-2.394.078h-.75c-1.076 0-1.827-.002-2.394-.078-.55-.074-.842-.21-1.05-.417-.182-.182-.307-.428-.385-.854-.081-.44-.102-1.022-.107-1.842a.563.563 0 00-1.125.006c.004.811.024 1.487.125 2.04.105.568.304 1.052.697 1.446.452.451 1.02.645 1.695.736.65.087 1.477.087 2.503.087h.832c1.026 0 1.853 0 2.503-.087.675-.09 1.243-.285 1.695-.736.451-.452.645-1.02.736-1.695.088-.65.088-1.477.088-2.503V5.96c0-1.026 0-1.853-.088-2.503-.09-.675-.285-1.243-.736-1.695-.452-.451-1.02-.645-1.695-.736-.65-.088-1.477-.088-2.503-.087h-.832z" />
|
||||
<path d="M11.25 8.438a.562.562 0 110 1.124H3.02l1.471 1.26a.563.563 0 01-.732.855l-2.625-2.25a.562.562 0 010-.854l2.625-2.25a.562.562 0 11.732.854l-1.47 1.26h8.229z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7095_11691">
|
||||
<rect width={18} height={18} rx={5} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronUpIcon } from "@/assets/icons";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownContent,
|
||||
DropdownTrigger,
|
||||
} from "@/components/ui/dropdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { LogOutIcon, SettingsIcon, UserIcon } from "./icons";
|
||||
|
||||
export function UserInfo() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const USER = {
|
||||
name: "John Smith",
|
||||
email: "johnson@nextadmin.com",
|
||||
img: "/images/user/user-03.png",
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={isOpen} setIsOpen={setIsOpen}>
|
||||
<DropdownTrigger className="rounded align-middle outline-none ring-primary ring-offset-2 focus-visible:ring-1 dark:ring-offset-gray-dark">
|
||||
<span className="sr-only">My Account</span>
|
||||
|
||||
<figure className="flex items-center gap-3">
|
||||
<Image
|
||||
src={USER.img}
|
||||
className="size-12"
|
||||
alt={`Avatar of ${USER.name}`}
|
||||
role="presentation"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<figcaption className="flex items-center gap-1 font-medium text-dark dark:text-dark-6 max-[1024px]:sr-only">
|
||||
<span>{USER.name}</span>
|
||||
|
||||
<ChevronUpIcon
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"rotate-180 transition-transform",
|
||||
isOpen && "rotate-0",
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent
|
||||
className="border border-stroke bg-white shadow-md dark:border-dark-3 dark:bg-gray-dark min-[230px]:min-w-[17.5rem]"
|
||||
align="end"
|
||||
>
|
||||
<h2 className="sr-only">User information</h2>
|
||||
|
||||
<figure className="flex items-center gap-2.5 px-5 py-3.5">
|
||||
<Image
|
||||
src={USER.img}
|
||||
className="size-12"
|
||||
alt={`Avatar for ${USER.name}`}
|
||||
role="presentation"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<figcaption className="space-y-1 text-base font-medium">
|
||||
<div className="mb-2 leading-none text-dark dark:text-white">
|
||||
{USER.name}
|
||||
</div>
|
||||
|
||||
<div className="leading-none text-gray-6">{USER.email}</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr className="border-[#E8E8E8] dark:border-dark-3" />
|
||||
|
||||
<div className="p-2 text-base text-[#4B5563] dark:text-dark-6 [&>*]:cursor-pointer">
|
||||
<Link
|
||||
href={"/profile"}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-[9px] hover:bg-gray-2 hover:text-dark dark:hover:bg-dark-3 dark:hover:text-white"
|
||||
>
|
||||
<UserIcon />
|
||||
|
||||
<span className="mr-auto text-base font-medium">View profile</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={"/pages/settings"}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-[9px] hover:bg-gray-2 hover:text-dark dark:hover:bg-dark-3 dark:hover:text-white"
|
||||
>
|
||||
<SettingsIcon />
|
||||
|
||||
<span className="mr-auto text-base font-medium">
|
||||
Account Settings
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<hr className="border-[#E8E8E8] dark:border-dark-3" />
|
||||
|
||||
<div className="p-2 text-base text-[#4B5563] dark:text-dark-6">
|
||||
<button
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-[9px] hover:bg-gray-2 hover:text-dark dark:hover:bg-dark-3 dark:hover:text-white"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<LogOutIcon />
|
||||
|
||||
<span className="text-base font-medium">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type PropsType = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ShowcaseSection({ title, children, className }: PropsType) {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="border-b border-stroke px-4 py-4 font-medium text-dark dark:border-dark-3 dark:text-white sm:px-6 xl:px-7.5">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className={cn("p-4 sm:p-6 xl:p-10", className)}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { title } from "process";
|
||||
import * as Icons from "../icons";
|
||||
import { z } from "zod";
|
||||
const navSubItemSchema: z.ZodType<any> = z.lazy(() =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
url: z.string(),
|
||||
icon: z.any(),
|
||||
items: z.array(navSubItemSchema).default([]),
|
||||
}),
|
||||
);
|
||||
|
||||
const navItemSchema = z.object({
|
||||
label: z.string(),
|
||||
items: z.array(navSubItemSchema),
|
||||
});
|
||||
|
||||
const navDataSchema = z.array(navItemSchema);
|
||||
export type NavSubItem = z.infer<typeof navSubItemSchema>;
|
||||
export type NavData = z.infer<typeof navDataSchema>;
|
||||
|
||||
export const NAV_DATA: NavData = [
|
||||
{
|
||||
label: "MAIN MENU",
|
||||
items: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/",
|
||||
icon: Icons.HomeIcon,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Admins",
|
||||
url: "/admins",
|
||||
icon: Icons.User,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "companies",
|
||||
url: "/companies",
|
||||
icon: Icons.Building,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Customers",
|
||||
url: "/customers",
|
||||
icon: Icons.User,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Tenders",
|
||||
url: "/tenders",
|
||||
icon: Icons.Table,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// label: "MAIN MENU",
|
||||
// items: [
|
||||
// {
|
||||
// title: "Dashboard",
|
||||
// icon: Icons.HomeIcon,
|
||||
// items: [
|
||||
// {
|
||||
// title: "eCommerce",
|
||||
// url: "/",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Calendar",
|
||||
// url: "/calendar",
|
||||
// icon: Icons.Calendar,
|
||||
// items: [],
|
||||
// },
|
||||
// {
|
||||
// title: "Profile",
|
||||
// url: "/profile",
|
||||
// icon: Icons.User,
|
||||
// items: [],
|
||||
// },
|
||||
// {
|
||||
// title: "Forms",
|
||||
// icon: Icons.Alphabet,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Form Elements",
|
||||
// url: "/forms/form-elements",
|
||||
// },
|
||||
// {
|
||||
// title: "Form Layout",
|
||||
// url: "/forms/form-layout",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Tables",
|
||||
// url: "/tables",
|
||||
// icon: Icons.Table,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Tables",
|
||||
// url: "/tables",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Pages",
|
||||
// icon: Icons.Alphabet,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Settings",
|
||||
// url: "/pages/settings",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// label: "OTHERS",
|
||||
// items: [
|
||||
// {
|
||||
// title: "Charts",
|
||||
// icon: Icons.PieChart,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Basic Chart",
|
||||
// url: "/charts/basic-chart",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "UI Elements",
|
||||
// icon: Icons.FourCircle,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Alerts",
|
||||
// url: "/ui-elements/alerts",
|
||||
// },
|
||||
// {
|
||||
// title: "Buttons",
|
||||
// url: "/ui-elements/buttons",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Authentication",
|
||||
// icon: Icons.Authentication,
|
||||
// items: [
|
||||
// {
|
||||
// title: "Sign In",
|
||||
// url: "/auth/sign-in",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
export const UI_ELEMENTS = ["Alerts", "Buttons"]
|
||||
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||||
.map((value) => ({
|
||||
title: value,
|
||||
url: "/ui-elements/" + value.split(" ").join("-").toLowerCase(),
|
||||
isPro: !["Alerts", "Buttons"].includes(value),
|
||||
}));
|
||||
@@ -0,0 +1,202 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export type PropsType = SVGProps<SVGSVGElement>;
|
||||
|
||||
export function ChevronUp(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={8}
|
||||
viewBox="0 0 16 8"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.553.728a.687.687 0 01.895 0l6.416 5.5a.688.688 0 01-.895 1.044L8 2.155 2.03 7.272a.688.688 0 11-.894-1.044l6.417-5.5z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Building(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 -960 960 960"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M80-120v-720h400v160h400v560H80Zm80-80h240v-80H160v80Zm0-160h240v-80H160v80Zm0-160h240v-80H160v80Zm0-160h240v-80H160v80Zm320 480h320v-400H480v400Zm80-240v-80h160v80H560Zm0 160v-80h160v80H560Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function HomeIcon(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M9 17.25a.75.75 0 000 1.5h6a.75.75 0 000-1.5H9z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 1.25c-.725 0-1.387.2-2.11.537-.702.327-1.512.81-2.528 1.415l-1.456.867c-1.119.667-2.01 1.198-2.686 1.706C2.523 6.3 2 6.84 1.66 7.551c-.342.711-.434 1.456-.405 2.325.029.841.176 1.864.36 3.146l.293 2.032c.237 1.65.426 2.959.707 3.978.29 1.05.702 1.885 1.445 2.524.742.64 1.63.925 2.716 1.062 1.056.132 2.387.132 4.066.132h2.316c1.68 0 3.01 0 4.066-.132 1.086-.137 1.974-.422 2.716-1.061.743-.64 1.155-1.474 1.445-2.525.281-1.02.47-2.328.707-3.978l.292-2.032c.185-1.282.332-2.305.36-3.146.03-.87-.062-1.614-.403-2.325C22 6.84 21.477 6.3 20.78 5.775c-.675-.508-1.567-1.039-2.686-1.706l-1.456-.867c-1.016-.605-1.826-1.088-2.527-1.415-.724-.338-1.386-.537-2.111-.537zM8.096 4.511c1.057-.63 1.803-1.073 2.428-1.365.609-.284 1.047-.396 1.476-.396.43 0 .867.112 1.476.396.625.292 1.37.735 2.428 1.365l1.385.825c1.165.694 1.986 1.184 2.59 1.638.587.443.91.809 1.11 1.225.199.416.282.894.257 1.626-.026.75-.16 1.691-.352 3.026l-.28 1.937c-.246 1.714-.422 2.928-.675 3.845-.247.896-.545 1.415-.977 1.787-.433.373-.994.593-1.925.71-.951.119-2.188.12-3.93.12h-2.213c-1.743 0-2.98-.001-3.931-.12-.93-.117-1.492-.337-1.925-.71-.432-.372-.73-.891-.977-1.787-.253-.917-.43-2.131-.676-3.845l-.279-1.937c-.192-1.335-.326-2.277-.352-3.026-.025-.732.058-1.21.258-1.626.2-.416.521-.782 1.11-1.225.603-.454 1.424-.944 2.589-1.638l1.385-.825z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Calendar(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M17 14a1 1 0 100-2 1 1 0 000 2zM17 18a1 1 0 100-2 1 1 0 000 2zM13 13a1 1 0 11-2 0 1 1 0 012 0zM13 17a1 1 0 11-2 0 1 1 0 012 0zM7 14a1 1 0 100-2 1 1 0 000 2zM7 18a1 1 0 100-2 1 1 0 000 2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7 1.75a.75.75 0 01.75.75v.763c.662-.013 1.391-.013 2.193-.013h4.113c.803 0 1.532 0 2.194.013V2.5a.75.75 0 011.5 0v.827c.26.02.506.045.739.076 1.172.158 2.121.49 2.87 1.238.748.749 1.08 1.698 1.238 2.87.153 1.14.153 2.595.153 4.433v2.112c0 1.838 0 3.294-.153 4.433-.158 1.172-.49 2.121-1.238 2.87-.749.748-1.698 1.08-2.87 1.238-1.14.153-2.595.153-4.433.153H9.944c-1.838 0-3.294 0-4.433-.153-1.172-.158-2.121-.49-2.87-1.238-.748-.749-1.08-1.698-1.238-2.87-.153-1.14-.153-2.595-.153-4.433v-2.112c0-1.838 0-3.294.153-4.433.158-1.172.49-2.121 1.238-2.87.749-.748 1.698-1.08 2.87-1.238.233-.031.48-.056.739-.076V2.5A.75.75 0 017 1.75zM5.71 4.89c-1.005.135-1.585.389-2.008.812-.423.423-.677 1.003-.812 2.009-.023.17-.042.35-.058.539h18.336c-.016-.19-.035-.369-.058-.54-.135-1.005-.389-1.585-.812-2.008-.423-.423-1.003-.677-2.009-.812-1.027-.138-2.382-.14-4.289-.14h-4c-1.907 0-3.261.002-4.29.14zM2.75 12c0-.854 0-1.597.013-2.25h18.474c.013.653.013 1.396.013 2.25v2c0 1.907-.002 3.262-.14 4.29-.135 1.005-.389 1.585-.812 2.008-.423.423-1.003.677-2.009.812-1.027.138-2.382.14-4.289.14h-4c-1.907 0-3.261-.002-4.29-.14-1.005-.135-1.585-.389-2.008-.812-.423-.423-.677-1.003-.812-2.009-.138-1.027-.14-2.382-.14-4.289v-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function User(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 1.25a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zM8.75 6a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0zM12 12.25c-2.313 0-4.445.526-6.024 1.414C4.42 14.54 3.25 15.866 3.25 17.5v.102c-.001 1.162-.002 2.62 1.277 3.662.629.512 1.51.877 2.7 1.117 1.192.242 2.747.369 4.773.369s3.58-.127 4.774-.369c1.19-.24 2.07-.605 2.7-1.117 1.279-1.042 1.277-2.5 1.276-3.662V17.5c0-1.634-1.17-2.96-2.725-3.836-1.58-.888-3.711-1.414-6.025-1.414zM4.75 17.5c0-.851.622-1.775 1.961-2.528 1.316-.74 3.184-1.222 5.29-1.222 2.104 0 3.972.482 5.288 1.222 1.34.753 1.961 1.677 1.961 2.528 0 1.308-.04 2.044-.724 2.6-.37.302-.99.597-2.05.811-1.057.214-2.502.339-4.476.339-1.974 0-3.42-.125-4.476-.339-1.06-.214-1.68-.509-2.05-.81-.684-.557-.724-1.293-.724-2.601z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Alphabet(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.25 7A.75.75 0 013 6.25h10a.75.75 0 010 1.5H3A.75.75 0 012.25 7zm14.25-.75a.75.75 0 01.684.442l4.5 10a.75.75 0 11-1.368.616l-1.437-3.194H14.12l-1.437 3.194a.75.75 0 11-1.368-.616l4.5-10a.75.75 0 01.684-.442zm-1.704 6.364h3.408L16.5 8.828l-1.704 3.786zM2.25 12a.75.75 0 01.75-.75h7a.75.75 0 010 1.5H3a.75.75 0 01-.75-.75zm0 5a.75.75 0 01.75-.75h5a.75.75 0 010 1.5H3a.75.75 0 01-.75-.75z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Table(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18.29 4.89c-1.028-.138-2.383-.14-4.29-.14h-4c-1.907 0-3.261.002-4.29.14-1.005.135-1.585.389-2.008.812-.423.423-.677 1.003-.812 2.009-.138 1.028-.14 2.382-.14 4.289 0 1.907.002 3.261.14 4.29.135 1.005.389 1.585.812 2.008.423.423 1.003.677 2.009.812 1.028.138 2.382.14 4.289.14h4c1.907 0 3.262-.002 4.29-.14 1.005-.135 1.585-.389 2.008-.812.423-.423.677-1.003.812-2.009.138-1.028.14-2.382.14-4.289 0-1.907-.002-3.261-.14-4.29-.135-1.005-.389-1.585-.812-2.008-.423-.423-1.003-.677-2.009-.812zm.199-1.487c1.172.158 2.121.49 2.87 1.238.748.749 1.08 1.698 1.238 2.87.153 1.14.153 2.595.153 4.433v.112c0 1.838 0 3.294-.153 4.433-.158 1.172-.49 2.121-1.238 2.87-.749.748-1.698 1.08-2.87 1.238-1.14.153-2.595.153-4.433.153H9.944c-1.838 0-3.294 0-4.433-.153-1.172-.158-2.121-.49-2.87-1.238-.748-.749-1.08-1.698-1.238-2.87-.153-1.14-.153-2.595-.153-4.433v-.112c0-1.838 0-3.294.153-4.433.158-1.172.49-2.121 1.238-2.87.749-.748 1.698-1.08 2.87-1.238 1.14-.153 2.595-.153 4.433-.153h4.112c1.838 0 3.294 0 4.433.153zM8.25 17a.75.75 0 01.75-.75h6a.75.75 0 010 1.5H9a.75.75 0 01-.75-.75z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function PieChart(props: PropsType) {
|
||||
return (
|
||||
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M14.254 1.365c-1.096-.306-2.122.024-2.851.695-.719.66-1.153 1.646-1.153 2.7v6.695a2.295 2.295 0 002.295 2.295h6.694c1.055 0 2.042-.434 2.701-1.153.67-.729 1.001-1.755.695-2.851a12.102 12.102 0 00-8.38-8.381zM11.75 4.76c0-.652.27-1.232.668-1.597.386-.355.886-.508 1.433-.355 3.55.991 6.349 3.79 7.34 7.34.153.548 0 1.048-.355 1.434-.365.397-.945.667-1.597.667h-6.694a.795.795 0 01-.795-.795V4.761z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M8.672 4.716a.75.75 0 00-.45-1.432C4.183 4.554 1.25 8.328 1.25 12.79c0 5.501 4.46 9.961 9.96 9.961 4.462 0 8.236-2.932 9.505-6.973a.75.75 0 10-1.43-.45 8.465 8.465 0 01-8.074 5.923 8.46 8.46 0 01-8.461-8.46 8.465 8.465 0 015.922-8.074z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function FourCircle(props: PropsType) {
|
||||
return (
|
||||
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.5 1.75a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zM3.25 6.5a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0zM17.5 12.75a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zm-3.25 4.75a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0zM12.75 6.5a4.75 4.75 0 119.5 0 4.75 4.75 0 01-9.5 0zm4.75-3.25a3.25 3.25 0 100 6.5 3.25 3.25 0 000-6.5zM6.5 12.75a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zM3.25 17.5a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Authentication(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M14.945 1.25c-1.367 0-2.47 0-3.337.117-.9.12-1.658.38-2.26.981-.524.525-.79 1.17-.929 1.928-.135.737-.161 1.638-.167 2.72a.75.75 0 001.5.008c.006-1.093.034-1.868.142-2.457.105-.566.272-.895.515-1.138.277-.277.666-.457 1.4-.556.755-.101 1.756-.103 3.191-.103h1c1.436 0 2.437.002 3.192.103.734.099 1.122.28 1.4.556.276.277.456.665.555 1.4.102.754.103 1.756.103 3.191v8c0 1.435-.001 2.436-.103 3.192-.099.734-.279 1.122-.556 1.399-.277.277-.665.457-1.399.556-.755.101-1.756.103-3.192.103h-1c-1.435 0-2.436-.002-3.192-.103-.733-.099-1.122-.28-1.399-.556-.243-.244-.41-.572-.515-1.138-.108-.589-.136-1.364-.142-2.457a.75.75 0 10-1.5.008c.006 1.082.032 1.983.167 2.72.14.758.405 1.403.93 1.928.601.602 1.36.86 2.26.982.866.116 1.969.116 3.336.116h1.11c1.368 0 2.47 0 3.337-.116.9-.122 1.658-.38 2.26-.982.602-.602.86-1.36.982-2.26.116-.867.116-1.97.116-3.337v-8.11c0-1.367 0-2.47-.116-3.337-.121-.9-.38-1.658-.982-2.26-.602-.602-1.36-.86-2.26-.981-.867-.117-1.97-.117-3.337-.117h-1.11z" />
|
||||
<path d="M2.001 11.249a.75.75 0 000 1.5h11.973l-1.961 1.68a.75.75 0 10.976 1.14l3.5-3a.75.75 0 000-1.14l-3.5-3a.75.75 0 00-.976 1.14l1.96 1.68H2.002z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArrowLeftIcon(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.89775 4.10225C8.11742 4.32192 8.11742 4.67808 7.89775 4.89775L4.358 8.4375H15C15.3107 8.4375 15.5625 8.68934 15.5625 9C15.5625 9.31066 15.3107 9.5625 15 9.5625H4.358L7.89775 13.1023C8.11742 13.3219 8.11742 13.6781 7.89775 13.8977C7.67808 14.1174 7.32192 14.1174 7.10225 13.8977L2.60225 9.39775C2.38258 9.17808 2.38258 8.82192 2.60225 8.60225L7.10225 4.10225C7.32192 3.88258 7.67808 3.88258 7.89775 4.10225Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { Logo } from "@/components/logo";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NAV_DATA, NavSubItem } from "./data";
|
||||
import { ArrowLeftIcon, ChevronUp } from "./icons";
|
||||
import { MenuItem } from "./menu-item";
|
||||
import { useSidebarContext } from "./sidebar-context";
|
||||
import Image from "next/image";
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const { setIsOpen, isOpen, isMobile, toggleSidebar } = useSidebarContext();
|
||||
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||
|
||||
const toggleExpanded = (title: string) => {
|
||||
// setExpandedItems((prev) => (prev.includes(title) ? [] : [title]));
|
||||
|
||||
// Uncomment the following line to enable multiple expanded items
|
||||
setExpandedItems((prev) =>
|
||||
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Keep collapsible open, when it's subpage is active
|
||||
NAV_DATA.some((section) => {
|
||||
return section.items.some((item) => {
|
||||
return item.items.some((subItem: NavSubItem) => {
|
||||
if (subItem.url === pathname) {
|
||||
if (!expandedItems.includes(item.title)) {
|
||||
toggleExpanded(item.title);
|
||||
}
|
||||
|
||||
// Break the loop
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Overlay */}
|
||||
{isMobile && isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
|
||||
onClick={() => setIsOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
"max-w-[290px] overflow-hidden border-r border-gray-200 bg-white transition-[width] duration-200 ease-linear dark:border-gray-800 dark:bg-gray-dark",
|
||||
isMobile ? "fixed bottom-0 top-0 z-50" : "sticky top-0 h-screen",
|
||||
isOpen ? "w-full" : "w-0",
|
||||
)}
|
||||
aria-label="Main navigation"
|
||||
aria-hidden={!isOpen}
|
||||
inert={!isOpen}
|
||||
>
|
||||
<div className="flex h-full flex-col py-10 pl-[25px] pr-[7px]">
|
||||
<div className="relative pr-4.5">
|
||||
<Link
|
||||
href={"/"}
|
||||
onClick={() => isMobile && toggleSidebar()}
|
||||
className="px-0 py-2.5 min-[850px]:py-0"
|
||||
>
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="absolute left-3/4 right-4.5 top-1/2 -translate-y-1/2 text-right"
|
||||
>
|
||||
<span className="sr-only">Close Menu</span>
|
||||
|
||||
<ArrowLeftIcon className="ml-auto size-7" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="custom-scrollbar mt-6 flex-1 overflow-y-auto pr-3 min-[850px]:mt-10">
|
||||
{NAV_DATA.map((section) => (
|
||||
<div key={section.label} className="mb-6">
|
||||
<h2 className="mb-5 text-sm font-medium text-dark-4 dark:text-dark-6">
|
||||
{section.label}
|
||||
</h2>
|
||||
|
||||
<nav role="navigation" aria-label={section.label}>
|
||||
<ul className="space-y-2">
|
||||
{section.items.map((item) => (
|
||||
<li key={item.title}>
|
||||
{item.items.length ? (
|
||||
<div>
|
||||
<MenuItem
|
||||
isActive={item.items.some(
|
||||
({ url }: { url: string }) => url === pathname,
|
||||
)}
|
||||
onClick={() => toggleExpanded(item.title)}
|
||||
>
|
||||
<item.icon
|
||||
className="size-6 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<span>{item.title}</span>
|
||||
|
||||
<ChevronUp
|
||||
className={cn(
|
||||
"ml-auto rotate-180 transition-transform duration-200",
|
||||
expandedItems.includes(item.title) &&
|
||||
"rotate-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
{expandedItems.includes(item.title) && (
|
||||
<ul
|
||||
className="ml-9 mr-0 space-y-1.5 pb-[15px] pr-0 pt-2"
|
||||
role="menu"
|
||||
>
|
||||
{item.items.map((subItem: NavSubItem) => (
|
||||
<li key={subItem.title} role="none">
|
||||
<MenuItem
|
||||
as="link"
|
||||
href={subItem.url}
|
||||
isActive={pathname === subItem.url}
|
||||
>
|
||||
<span>{subItem.title}</span>
|
||||
</MenuItem>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const href =
|
||||
"url" in item
|
||||
? item.url + ""
|
||||
: "/" +
|
||||
item.title.toLowerCase().split(" ").join("-");
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className="flex items-center gap-3 py-3"
|
||||
as="link"
|
||||
href={href}
|
||||
isActive={pathname === href}
|
||||
>
|
||||
<item.icon
|
||||
className="size-6 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<span>{item.title}</span>
|
||||
</MenuItem>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
import Link from "next/link";
|
||||
import { useSidebarContext } from "./sidebar-context";
|
||||
|
||||
const menuItemBaseStyles = cva(
|
||||
"rounded-lg px-3.5 font-medium text-dark-4 transition-all duration-200 dark:text-dark-6",
|
||||
{
|
||||
variants: {
|
||||
isActive: {
|
||||
true: "bg-[rgba(87,80,241,0.07)] text-primary hover:bg-[rgba(87,80,241,0.07)] dark:bg-[#FFFFFF1A] dark:text-white",
|
||||
false:
|
||||
"hover:bg-gray-100 hover:text-dark hover:dark:bg-[#FFFFFF1A] hover:dark:text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function MenuItem(
|
||||
props: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
isActive: boolean;
|
||||
} & ({ as?: "button"; onClick: () => void } | { as: "link"; href: string }),
|
||||
) {
|
||||
const { toggleSidebar, isMobile } = useSidebarContext();
|
||||
|
||||
if (props.as === "link") {
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
// Close sidebar on clicking link if it's mobile
|
||||
onClick={() => isMobile && toggleSidebar()}
|
||||
className={cn(
|
||||
menuItemBaseStyles({
|
||||
isActive: props.isActive,
|
||||
className: "relative block py-2",
|
||||
}),
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
aria-expanded={props.isActive}
|
||||
className={menuItemBaseStyles({
|
||||
isActive: props.isActive,
|
||||
className: "flex w-full items-center gap-3 py-3",
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type SidebarState = "expanded" | "collapsed";
|
||||
|
||||
type SidebarContextType = {
|
||||
state: SidebarState;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = createContext<SidebarContextType | null>(null);
|
||||
|
||||
export function useSidebarContext() {
|
||||
const context = useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebarContext must be used within a SidebarProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function SidebarProvider({
|
||||
children,
|
||||
defaultOpen = true,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
function toggleSidebar() {
|
||||
setIsOpen((prev) => !prev);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
state: isOpen ? "expanded" : "collapsed",
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
isMobile,
|
||||
toggleSidebar,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export function AdminsSkeleton() {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Admins
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-center">
|
||||
<TableHead className="!text-left">Full name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead className="!text-right">Role</TableHead>
|
||||
<TableHead>Username</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAdminsPresenter } from "./useAdminsPresenter";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import { AdminsSkeleton } from "./Skeleton";
|
||||
import Link from "next/link";
|
||||
|
||||
const AdminsTable = () => {
|
||||
const {
|
||||
allUsers,
|
||||
router,
|
||||
isPending,
|
||||
onConfirmDelete,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
modalConfig,
|
||||
setCurrentUser,
|
||||
pathName,
|
||||
} = useAdminsPresenter();
|
||||
|
||||
if (isPending) return <AdminsSkeleton />;
|
||||
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">
|
||||
<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 Admin</Link>
|
||||
</button>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Full name
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Email
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Role
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Username
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Actions
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{allUsers.map((item) => {
|
||||
return (
|
||||
<TableRow key={item.id} className="odd:bg-gray-2 dark:odd:bg-gray-7">
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.full_name}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.email}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.role}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.username}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Delete Admin </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
className="hover:text-primary"
|
||||
onClick={() => router.push(`/admins/edit/${item.id}`)}
|
||||
>
|
||||
<span className="sr-only">Edit Admin </span>
|
||||
<PencilSquareIcon />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
title={modalConfig.title || "Confirm"}
|
||||
message={modalConfig.message || "Are you sure?"}
|
||||
variant={modalConfig.variant || "warning"}
|
||||
size={modalConfig.size || "md"}
|
||||
confirmText={modalConfig.confirmText ?? "Confirm"}
|
||||
cancelText={modalConfig.cancelText ?? "Cancel"}
|
||||
onConfirm={() => {
|
||||
onConfirmDelete();
|
||||
}}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminsTable;
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useAdminListInfiniteQuery, useDeleteAdmin } from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { IUser } from "@/lib/api";
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
export const useAdminsPresenter = () => {
|
||||
const [currentUser, setCurrentUser] = useState<IUser | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
Partial<ConfirmationModalProps>
|
||||
>({});
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
const pathName = usePathname();
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useAdminListInfiniteQuery(params);
|
||||
const { mutate: deleteAdmin } = useDeleteAdmin(() => {
|
||||
setIsModalOpen(false);
|
||||
});
|
||||
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const result = z.string().safeParse(debouncedSearch);
|
||||
if (result.success) {
|
||||
setParams((prevParams) => ({
|
||||
...prevParams,
|
||||
}));
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
const onConfirmDelete = () => {
|
||||
if (currentUser) {
|
||||
deleteAdmin(currentUser.id);
|
||||
}
|
||||
};
|
||||
const allUsers = data?.pages.flatMap((page) => page.data.users) ?? [];
|
||||
return {
|
||||
allUsers,
|
||||
currentAdmin: currentUser,
|
||||
error,
|
||||
handleFilterChange,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
isPending,
|
||||
onConfirmDelete,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
search,
|
||||
hasNextPage,
|
||||
data,
|
||||
router,
|
||||
setCurrentUser,
|
||||
pathName,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
||||
export function CompaniesSkeleton() {
|
||||
const { columns } = useCompanyListPresenter();
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Companies
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-center">
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
colSpan={100}
|
||||
className="text-start font-extrabold"
|
||||
key={column}
|
||||
>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { CompaniesSkeleton } from "./Skeleton";
|
||||
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const CompaniesTable = ({}: IProps) => {
|
||||
const {
|
||||
isPending,
|
||||
pathName,
|
||||
columns,
|
||||
allCompanies,
|
||||
setIsModalOpen,
|
||||
router,
|
||||
isModalOpen,
|
||||
setCurrentCompany,
|
||||
onConfirmDelete,
|
||||
modalConfig,
|
||||
} = useCompanyListPresenter();
|
||||
if (isPending) return <CompaniesSkeleton />;
|
||||
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">
|
||||
<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 Company</Link>
|
||||
</button>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((columns) => (
|
||||
<TableHead
|
||||
colSpan={100}
|
||||
className="text-start font-extrabold"
|
||||
key={columns}
|
||||
>
|
||||
{columns}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{allCompanies.map((company) =>
|
||||
!company ? (
|
||||
<h6>No Data was Found</h6>
|
||||
) : (
|
||||
<TableRow key={company.id} className="odd:bg-gray-2 dark:odd:bg-gray-7">
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.name}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.email}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.phone}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.address.country}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.address.state}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.address.city}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.address.postal_code}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.language}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.currency}
|
||||
</TableHead>
|
||||
<TableHead className="text-start" colSpan={100}>
|
||||
{company.employee_count}
|
||||
</TableHead>
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
setCurrentCompany(company);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Delete Company </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
className="hover:text-primary"
|
||||
onClick={() =>
|
||||
router.push(`${pathName}/edit/${company.id}`)
|
||||
}
|
||||
>
|
||||
<span className="sr-only">Edit Company </span>
|
||||
<PencilSquareIcon />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
title={modalConfig.title || "Confirm"}
|
||||
message={modalConfig.message || "Are you sure?"}
|
||||
variant={modalConfig.variant || "default"}
|
||||
size={modalConfig.size || "md"}
|
||||
confirmText={modalConfig.confirmText ?? "Confirm"}
|
||||
cancelText={modalConfig.cancelText ?? "Cancel"}
|
||||
onConfirm={() => {
|
||||
onConfirmDelete();
|
||||
}}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompaniesTable;
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
useCompaniesInfiniteQuery,
|
||||
useDeleteCompanyQuery,
|
||||
} from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { TCompany } from "@/lib/api";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { z } from "zod";
|
||||
|
||||
export const useCompanyListPresenter = () => {
|
||||
const deleteModal = useRef<HTMLDialogElement | null>(null);
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
const [currentCompany, setCurrentCompany] = useState<TCompany | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
Partial<ConfirmationModalProps>
|
||||
>({});
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const columns = [
|
||||
"name",
|
||||
"email",
|
||||
"phone",
|
||||
"country",
|
||||
"state",
|
||||
"city",
|
||||
"postal_code",
|
||||
"language",
|
||||
"currency",
|
||||
"employee count",
|
||||
"actions",
|
||||
];
|
||||
const pathName = usePathname();
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
isError,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useCompaniesInfiniteQuery(params);
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
||||
const { mutate: deleteCompany } = useDeleteCompanyQuery(() =>
|
||||
setIsModalOpen(false),
|
||||
);
|
||||
useEffect(() => {
|
||||
const result = z.string().safeParse(debouncedSearch);
|
||||
if (result.success) {
|
||||
setParams((prevParams) => ({
|
||||
...prevParams,
|
||||
}));
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
const handleFilterChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
const onConfirmDelete = () => {
|
||||
if (currentCompany) {
|
||||
deleteCompany(currentCompany.id);
|
||||
}
|
||||
};
|
||||
const allCompanies = data?.pages.flatMap((page) => page.data.companies) ?? [];
|
||||
return {
|
||||
allCompanies,
|
||||
handleFilterChange,
|
||||
onConfirmDelete,
|
||||
deleteCompany,
|
||||
isFetchingNextPage,
|
||||
data,
|
||||
columns,
|
||||
pathName,
|
||||
currentCompany,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
setCurrentCompany,
|
||||
deleteModal,
|
||||
error,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
router,
|
||||
isPending,
|
||||
isError,
|
||||
search,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export function CustomersSkeleton() {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Tenders
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-center">
|
||||
<TableHead className="uppercase">full name</TableHead>
|
||||
<TableHead className="uppercase">email</TableHead>
|
||||
<TableHead className="uppercase">created at</TableHead>
|
||||
<TableHead className="uppercase">type</TableHead>
|
||||
<TableHead className="uppercase">status</TableHead>
|
||||
<TableHead className="uppercase">company</TableHead>
|
||||
<TableHead className="uppercase">actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { CustomersSkeleton } from "./Skeleton";
|
||||
import useCustomerListPresenter from "./useCustomerListPresenter";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
const CustomersTable = () => {
|
||||
const {
|
||||
isPending,
|
||||
pathName,
|
||||
router,
|
||||
columns,
|
||||
allCustomers,
|
||||
modalConfig,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
onConfirmDelete,
|
||||
setCurrentCustomer,
|
||||
} = useCustomerListPresenter();
|
||||
if (isPending) return <CustomersSkeleton />;
|
||||
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">
|
||||
<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 Company</Link>
|
||||
</button>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((columns) => (
|
||||
<TableHead
|
||||
colSpan={100}
|
||||
className="text-start font-extrabold uppercase"
|
||||
key={columns}
|
||||
>
|
||||
{columns}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allCustomers.map((customer, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell colSpan={100}>{customer.full_name}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.email}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.created_at}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.type}</TableCell>
|
||||
<TableCell colSpan={100}>{customer.status}</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{customer.companies?.length ? (
|
||||
customer.companies.map((c) => <span>{c.name}, </span>)
|
||||
) : (
|
||||
<span>None</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
setCurrentCustomer(customer);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Delete Company </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
className="hover:text-primary"
|
||||
onClick={() =>
|
||||
router.push(`${pathName}/edit/${customer.id}`)
|
||||
}
|
||||
>
|
||||
<span className="sr-only">Edit Company </span>
|
||||
<PencilSquareIcon />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
title={modalConfig.title || "Confirm"}
|
||||
message={modalConfig.message || "Are you sure?"}
|
||||
variant={modalConfig.variant || "default"}
|
||||
size={modalConfig.size || "md"}
|
||||
confirmText={modalConfig.confirmText ?? "Confirm"}
|
||||
cancelText={modalConfig.cancelText ?? "Cancel"}
|
||||
onConfirm={() => {
|
||||
onConfirmDelete();
|
||||
}}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomersTable;
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import {
|
||||
useAssignCompany,
|
||||
useCustomersInfiniteQuery,
|
||||
useDeleteCustomerQuery,
|
||||
} from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { z } from "zod";
|
||||
|
||||
const useCustomerListPresenter = () => {
|
||||
const deleteModal = useRef<HTMLDialogElement | null>(null);
|
||||
const assignToCompanyModal = useRef<HTMLDialogElement | null>(null);
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
const [selectedCompanies, setSelectedCompanies] =
|
||||
useState<TAssignCustomerToCompanyCredentials | null>(null);
|
||||
const [currentCustomer, setCurrentCustomer] = useState<TCustomer | null>(
|
||||
null,
|
||||
);
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
Partial<ConfirmationModalProps>
|
||||
>({});
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const pathName = usePathname();
|
||||
const [search, setSearch] = useState("");
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const router = useRouter();
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
isError,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useCustomersInfiniteQuery(params);
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
||||
const { mutate: deleteCustomer } = useDeleteCustomerQuery(() =>
|
||||
setIsModalOpen(false),
|
||||
);
|
||||
const { mutate: assignSelectedCompanies } = useAssignCompany(
|
||||
currentCustomer?.id ?? "",
|
||||
() => {
|
||||
assignToCompanyModal.current?.close();
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
const result = z.string().safeParse(debouncedSearch);
|
||||
if (result.success) {
|
||||
setParams((prevParams) => ({
|
||||
...prevParams,
|
||||
}));
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
const handleFilterChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
const columns = [
|
||||
"full name",
|
||||
"email",
|
||||
"created at",
|
||||
"type",
|
||||
"status",
|
||||
"company",
|
||||
"actions",
|
||||
];
|
||||
const onConfirmDelete = () => {
|
||||
if (currentCustomer) {
|
||||
deleteCustomer(currentCustomer.id);
|
||||
}
|
||||
};
|
||||
const customers = data?.pages.flatMap((page) => page.data.customers) ?? [];
|
||||
return {
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
allCustomers: customers,
|
||||
currentCustomer,
|
||||
deleteCustomer,
|
||||
deleteModal,
|
||||
error,
|
||||
handleFilterChange,
|
||||
isError,
|
||||
isFetchingNextPage,
|
||||
columns,
|
||||
data,
|
||||
isPending,
|
||||
search,
|
||||
setCurrentCustomer,
|
||||
selectedCompanies,
|
||||
setSelectedCompanies,
|
||||
onConfirmDelete,
|
||||
assignToCompanyModal,
|
||||
assignSelectedCompanies,
|
||||
pathName,
|
||||
router,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCustomerListPresenter;
|
||||
@@ -0,0 +1,121 @@
|
||||
import * as logos from "@/assets/logos";
|
||||
|
||||
export async function getTopProducts() {
|
||||
// Fake delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
return [
|
||||
{
|
||||
image: "/images/product/product-01.png",
|
||||
name: "Apple Watch Series 7",
|
||||
category: "Electronics",
|
||||
price: 296,
|
||||
sold: 22,
|
||||
profit: 45,
|
||||
},
|
||||
{
|
||||
image: "/images/product/product-02.png",
|
||||
name: "Macbook Pro M1",
|
||||
category: "Electronics",
|
||||
price: 546,
|
||||
sold: 12,
|
||||
profit: 125,
|
||||
},
|
||||
{
|
||||
image: "/images/product/product-03.png",
|
||||
name: "Dell Inspiron 15",
|
||||
category: "Electronics",
|
||||
price: 443,
|
||||
sold: 64,
|
||||
profit: 247,
|
||||
},
|
||||
{
|
||||
image: "/images/product/product-04.png",
|
||||
name: "HP Probook 450",
|
||||
category: "Electronics",
|
||||
price: 499,
|
||||
sold: 72,
|
||||
profit: 103,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function getInvoiceTableData() {
|
||||
// Fake delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1400));
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Free package",
|
||||
price: 0.0,
|
||||
date: "2023-01-13T18:00:00.000Z",
|
||||
status: "Paid",
|
||||
},
|
||||
{
|
||||
name: "Standard Package",
|
||||
price: 59.0,
|
||||
date: "2023-01-13T18:00:00.000Z",
|
||||
status: "Paid",
|
||||
},
|
||||
{
|
||||
name: "Business Package",
|
||||
price: 99.0,
|
||||
date: "2023-01-13T18:00:00.000Z",
|
||||
status: "Unpaid",
|
||||
},
|
||||
{
|
||||
name: "Standard Package",
|
||||
price: 59.0,
|
||||
date: "2023-01-13T18:00:00.000Z",
|
||||
status: "Pending",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function getTopChannels() {
|
||||
// Fake delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Google",
|
||||
visitors: 3456,
|
||||
revenues: 4220,
|
||||
sales: 3456,
|
||||
conversion: 2.59,
|
||||
logo: logos.google,
|
||||
},
|
||||
{
|
||||
name: "X.com",
|
||||
visitors: 3456,
|
||||
revenues: 4220,
|
||||
sales: 3456,
|
||||
conversion: 2.59,
|
||||
logo: logos.x,
|
||||
},
|
||||
{
|
||||
name: "Github",
|
||||
visitors: 3456,
|
||||
revenues: 4220,
|
||||
sales: 3456,
|
||||
conversion: 2.59,
|
||||
logo: logos.github,
|
||||
},
|
||||
{
|
||||
name: "Vimeo",
|
||||
visitors: 3456,
|
||||
revenues: 4220,
|
||||
sales: 3456,
|
||||
conversion: 2.59,
|
||||
logo: logos.vimeo,
|
||||
},
|
||||
{
|
||||
name: "Facebook",
|
||||
visitors: 3456,
|
||||
revenues: 4220,
|
||||
sales: 3456,
|
||||
conversion: 2.59,
|
||||
logo: logos.facebook,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IconProps } from "@/types/icon-props";
|
||||
|
||||
export function PreviewIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10 6.875a3.125 3.125 0 100 6.25 3.125 3.125 0 000-6.25zM8.123 10a1.875 1.875 0 113.75 0 1.875 1.875 0 01-3.75 0z" />
|
||||
<path d="M10 2.708c-3.762 0-6.296 2.254-7.767 4.164l-.026.035c-.333.432-.64.83-.847 1.3C1.137 8.71 1.04 9.26 1.04 10s.096 1.29.319 1.793c.208.47.514.868.847 1.3l.026.034c1.47 1.911 4.005 4.165 7.766 4.165 3.762 0 6.296-2.254 7.766-4.165l.027-.034c.333-.432.639-.83.847-1.3.222-.504.319-1.053.319-1.793s-.097-1.29-.32-1.793c-.207-.47-.513-.868-.846-1.3l-.027-.035c-1.47-1.91-4.004-4.164-7.766-4.164zM3.223 7.635C4.582 5.87 6.79 3.958 9.999 3.958s5.418 1.913 6.776 3.677c.366.475.58.758.72 1.077.132.298.213.662.213 1.288s-.081.99-.213 1.288c-.14.319-.355.602-.72 1.077-1.358 1.764-3.568 3.677-6.776 3.677-3.208 0-5.417-1.913-6.775-3.677-.366-.475-.58-.758-.72-1.077-.132-.298-.213-.662-.213-1.288s.08-.99.212-1.288c.141-.319.355-.602.72-1.077z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function DownloadIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10.461 13.755a.625.625 0 01-.922 0L6.205 10.11a.625.625 0 11.923-.843l2.247 2.457V2.5a.625.625 0 111.25 0v9.223l2.247-2.457a.625.625 0 01.923.843l-3.334 3.646z" />
|
||||
<path d="M3.125 12.5a.625.625 0 10-1.25 0v.046c0 1.14 0 2.058.097 2.78.101.75.317 1.382.818 1.884.502.501 1.133.717 1.884.818.722.097 1.64.097 2.78.097h5.092c1.14 0 2.058 0 2.78-.097.75-.101 1.382-.317 1.884-.818.501-.502.717-1.134.818-1.884.097-.722.097-1.64.097-2.78V12.5a.625.625 0 10-1.25 0c0 1.196-.001 2.03-.086 2.66-.082.611-.233.935-.463 1.166-.23.23-.555.38-1.166.463-.63.085-1.464.086-2.66.086h-5c-1.196 0-2.03-.001-2.66-.086-.611-.082-.935-.233-1.166-.463-.23-.23-.38-.555-.463-1.166-.085-.63-.086-1.464-.086-2.66z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { TrashIcon } from "@/assets/icons";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import dayjs from "dayjs";
|
||||
import { getInvoiceTableData } from "./fetch";
|
||||
import { DownloadIcon, PreviewIcon } from "./icons";
|
||||
|
||||
export async function InvoiceTable() {
|
||||
const data = await getInvoiceTableData();
|
||||
|
||||
return (
|
||||
<div className="rounded-[10px] border border-stroke bg-white p-4 shadow-1 dark:border-dark-3 dark:bg-gray-dark dark:shadow-card sm:p-7.5">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none bg-[#F7F9FC] dark:bg-dark-2 [&>th]:py-4 [&>th]:text-base [&>th]:text-dark [&>th]:dark:text-white">
|
||||
<TableHead className="min-w-[155px] xl:pl-7.5">Package</TableHead>
|
||||
<TableHead>Invoice Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right xl:pr-7.5">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow key={index} className="border-[#eee] dark:border-dark-3">
|
||||
<TableCell className="min-w-[155px] xl:pl-7.5">
|
||||
<h5 className="text-dark dark:text-white">{item.name}</h5>
|
||||
<p className="mt-[3px] text-body-sm font-medium">
|
||||
${item.price}
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<p className="text-dark dark:text-white">
|
||||
{dayjs(item.date).format("MMM DD, YYYY")}
|
||||
</p>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-fit rounded-full px-3.5 py-1 text-sm font-medium",
|
||||
{
|
||||
"bg-[#219653]/[0.08] text-[#219653]":
|
||||
item.status === "Paid",
|
||||
"bg-[#D34053]/[0.08] text-[#D34053]":
|
||||
item.status === "Unpaid",
|
||||
"bg-[#FFA70B]/[0.08] text-[#FFA70B]":
|
||||
item.status === "Pending",
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.status}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="xl:pr-7.5">
|
||||
<div className="flex items-center justify-end gap-x-3.5">
|
||||
<button className="hover:text-primary">
|
||||
<span className="sr-only">View Invoice</span>
|
||||
<PreviewIcon />
|
||||
</button>
|
||||
|
||||
<button className="hover:text-primary">
|
||||
<span className="sr-only">Delete Invoice</span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
|
||||
<button className="hover:text-primary">
|
||||
<span className="sr-only">Download Invoice</span>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export function TendersSkeleton() {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Tenders
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-center">
|
||||
<TableHead>Full name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Company</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import useTenderListPresenter from "./useTenderListPresenter";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
import { TendersSkeleton } from "./Skeleton";
|
||||
const TendersTable = () => {
|
||||
const { allTenders, isPending } = useTenderListPresenter();
|
||||
if (isPending) return <TendersSkeleton />;
|
||||
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 className="border-none uppercase [&>th]:text-start">
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Title
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Country Code
|
||||
</TableHead>
|
||||
<TableHead colSpan={100} className="text-start font-extrabold">
|
||||
Status
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{allTenders.map((item) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.title}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.country_code}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.status}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TendersTable;
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import { useTendersInfiniteQuery } from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { TCustomer } from "@/lib/api";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { z } from "zod";
|
||||
const useTenderListPresenter = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [currentTender, setCurrentTender] = useState<TCustomer | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
Partial<ConfirmationModalProps>
|
||||
>({});
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
sort: "created_at",
|
||||
});
|
||||
const debouncedSearch = useDebounce(search, 1000);
|
||||
const pathName = usePathname();
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useTendersInfiniteQuery(params);
|
||||
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const result = z.string().safeParse(debouncedSearch);
|
||||
if (result.success) {
|
||||
setParams((prevParams) => ({
|
||||
...prevParams,
|
||||
q: result.data,
|
||||
}));
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
const onConfirmDelete = () => {
|
||||
if (currentTender) {
|
||||
deleteTender(currentTender.id);
|
||||
}
|
||||
};
|
||||
|
||||
const allTenders = data?.pages.flatMap((page) => page.data.tenders) ?? [];
|
||||
return {
|
||||
currentTender,
|
||||
setCurrentTender,
|
||||
search,
|
||||
handleFilterChange,
|
||||
onConfirmDelete,
|
||||
allTenders,
|
||||
error,
|
||||
isPending,
|
||||
hasNextPage,
|
||||
data,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
pathName,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTenderListPresenter;
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { compactFormat, standardFormat } from "@/lib/format-number";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { getTopChannels } from "../fetch";
|
||||
|
||||
export async function TopChannels({ className }: { className?: string }) {
|
||||
const data = await getTopChannels();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<h2 className="mb-4 text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Top Channels
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-center">
|
||||
<TableHead className="min-w-[120px] !text-left">Source</TableHead>
|
||||
<TableHead>Visitors</TableHead>
|
||||
<TableHead className="!text-right">Revenues</TableHead>
|
||||
<TableHead>Sales</TableHead>
|
||||
<TableHead>Conversion</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{data.map((channel, i) => (
|
||||
<TableRow
|
||||
className="text-center text-base font-medium text-dark dark:text-white"
|
||||
key={channel.name + i}
|
||||
>
|
||||
<TableCell className="flex min-w-fit items-center gap-3">
|
||||
<Image
|
||||
src={channel.logo}
|
||||
className="size-8 rounded-full object-cover"
|
||||
width={40}
|
||||
height={40}
|
||||
alt={channel.name + " Logo"}
|
||||
role="presentation"
|
||||
/>
|
||||
<div className="">{channel.name}</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{compactFormat(channel.visitors)}</TableCell>
|
||||
|
||||
<TableCell className="!text-right text-green-light-1">
|
||||
${standardFormat(channel.revenues)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{channel.sales}</TableCell>
|
||||
|
||||
<TableCell>{channel.conversion}%</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export function TopChannelsSkeleton() {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="mb-5.5 text-body-2xlg font-bold text-dark dark:text-white">
|
||||
Top Channels
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-center">
|
||||
<TableHead className="!text-left">Source</TableHead>
|
||||
<TableHead>Visitors</TableHead>
|
||||
<TableHead className="!text-right">Revenues</TableHead>
|
||||
<TableHead>Sales</TableHead>
|
||||
<TableHead>Conversion</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import Image from "next/image";
|
||||
import { getTopProducts } from "../fetch";
|
||||
|
||||
export async function TopProducts() {
|
||||
const data = await getTopProducts();
|
||||
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<div className="px-6 py-4 sm:px-7 sm:py-5 xl:px-8.5">
|
||||
<h2 className="text-2xl font-bold text-dark dark:text-white">
|
||||
Top Products
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-t text-base [&>th]:h-auto [&>th]:py-3 sm:[&>th]:py-4.5">
|
||||
<TableHead className="min-w-[120px] pl-5 sm:pl-6 xl:pl-7.5">
|
||||
Product Name
|
||||
</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Sold</TableHead>
|
||||
<TableHead className="pr-5 text-right sm:pr-6 xl:pr-7.5">
|
||||
Profit
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{data.map((product) => (
|
||||
<TableRow
|
||||
className="text-base font-medium text-dark dark:text-white"
|
||||
key={product.name + product.profit}
|
||||
>
|
||||
<TableCell className="flex min-w-fit items-center gap-3 pl-5 sm:pl-6 xl:pl-7.5">
|
||||
<Image
|
||||
src={product.image}
|
||||
className="aspect-[6/5] w-15 rounded-[5px] object-cover"
|
||||
width={60}
|
||||
height={50}
|
||||
alt={"Image for product " + product.name}
|
||||
role="presentation"
|
||||
/>
|
||||
<div>{product.name}</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{product.category}</TableCell>
|
||||
|
||||
<TableCell>${product.price}</TableCell>
|
||||
|
||||
<TableCell>{product.sold}</TableCell>
|
||||
|
||||
<TableCell className="pr-5 text-right text-green-light-1 sm:pr-6 xl:pr-7.5">
|
||||
${product.profit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export function TopProductsSkeleton() {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="px-4 py-6 text-2xl font-bold text-dark dark:text-white md:px-6 xl:px-9">
|
||||
Top Products
|
||||
</h2>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-t text-base [&>th]:h-auto [&>th]:py-3 sm:[&>th]:py-4.5">
|
||||
<TableHead className="min-w-[120px]">Product Name</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Sold</TableHead>
|
||||
<TableHead>Profit</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell colSpan={100}>
|
||||
<Skeleton className="h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
"use client";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import useCreateAdminPresenter from "./useCreateAdminPresenter";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { ICreateAdminCredentials } from "@/lib/api";
|
||||
import { FC, useEffect } from "react";
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: ICreateAdminCredentials;
|
||||
id?: string;
|
||||
}
|
||||
const CreateAdminForm: FC<IProps> = ({ defaultValues, editMode, id }) => {
|
||||
const { errors, handleSubmit, register, router, onSubmit } =
|
||||
useCreateAdminPresenter({
|
||||
editMode,
|
||||
defaultValues,
|
||||
id,
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid grid-cols-1 gap-9"
|
||||
onSubmit={handleSubmit((data) => {
|
||||
onSubmit(data);
|
||||
})}
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
title="Input Fields"
|
||||
className="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<div>
|
||||
<InputGroup
|
||||
{...register("full_name", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 5,
|
||||
message: FormErrorMessages.minLength(5),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="Full name"
|
||||
placeholder="Full name"
|
||||
type="text"
|
||||
name="full_name"
|
||||
/>
|
||||
{errors.full_name && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.full_name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
label="Username"
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
{...register("username", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9]+$/,
|
||||
message: FormErrorMessages.alphaNumeric,
|
||||
},
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: FormErrorMessages.minLength(3),
|
||||
},
|
||||
maxLength: {
|
||||
value: 30,
|
||||
message: FormErrorMessages.maxLength(30),
|
||||
},
|
||||
})}
|
||||
name="username"
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.username.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
{...register("email", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
pattern: {
|
||||
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: FormErrorMessages.invalidEmail,
|
||||
},
|
||||
})}
|
||||
name="email"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
{...register("password", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: FormErrorMessages.minLength(8),
|
||||
},
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: FormErrorMessages.maxLength(128),
|
||||
},
|
||||
})}
|
||||
name="password"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
label="Phone"
|
||||
placeholder="Phone"
|
||||
type="text"
|
||||
{...register("phone", {
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
name="phone"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.phone.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
label="Department"
|
||||
placeholder="Department"
|
||||
type="text"
|
||||
{...register("department", {
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
name="department"
|
||||
/>
|
||||
{errors.department && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.department.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
label="Position"
|
||||
placeholder="Position"
|
||||
type="text"
|
||||
{...register("position", {
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
name="position"
|
||||
/>
|
||||
{errors.position && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.position.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputGroup
|
||||
type="file"
|
||||
fileStyleVariant="style1"
|
||||
label="Attach file"
|
||||
placeholder="Attach file"
|
||||
register={register}
|
||||
name="profile_image"
|
||||
/>
|
||||
{errors.profile_image && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.profile_image.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex gap-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={router.back}
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Opt out
|
||||
</button>
|
||||
</div>
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAdminForm;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useCreateAdmin, useUpdateAdmin } from "@/hooks/queries";
|
||||
import { ICreateAdminCredentials } from "@/lib/api";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: ICreateAdminCredentials;
|
||||
id?: string;
|
||||
}
|
||||
const useCreateAdminPresenter = ({
|
||||
editMode = false,
|
||||
defaultValues,
|
||||
id,
|
||||
}: IProps) => {
|
||||
const isMutating = useIsMutating();
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = useForm<ICreateAdminCredentials>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { mutate: createAdmin } = useCreateAdmin();
|
||||
const { mutate: updateAdmin } = useUpdateAdmin(id as string);
|
||||
const onSubmit: SubmitHandler<ICreateAdminCredentials> = (data) => {
|
||||
if (editMode) {
|
||||
updateAdmin({ id: id as string, credentials: data });
|
||||
} else {
|
||||
createAdmin({ ...data, role: "admin" });
|
||||
}
|
||||
};
|
||||
const router = useRouter();
|
||||
return {
|
||||
errors,
|
||||
register,
|
||||
handleSubmit,
|
||||
router,
|
||||
isMutating,
|
||||
onSubmit,
|
||||
watchForm: watch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateAdminPresenter;
|
||||
@@ -0,0 +1,644 @@
|
||||
"use client";
|
||||
import { ICreateCompanyCredentials } from "@/lib/api/types/Company";
|
||||
import useCreateCompanyPresenter from "./useCreateCompanyPresenter";
|
||||
import { ShowcaseSection } from "../../Layouts/showcase-section";
|
||||
import { title } from "process";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { CurrencyIcon, GlobeIcon, MoneyIcon } from "@/assets/icons";
|
||||
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: ICreateCompanyCredentials;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
const {
|
||||
errors,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
isCreating,
|
||||
isMutating,
|
||||
isUpdating,
|
||||
onSubmit,
|
||||
router,
|
||||
register,
|
||||
} = useCreateCompanyPresenter({
|
||||
editMode,
|
||||
defaultValues,
|
||||
id,
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 sm:grid-cols-2"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
title="Company Information"
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<InputGroup
|
||||
{...register("name", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 200,
|
||||
message: FormErrorMessages.maxLength(200),
|
||||
},
|
||||
})}
|
||||
label="Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Enter Company Name"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.name.message}</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("email", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
pattern: {
|
||||
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: FormErrorMessages.invalidEmail,
|
||||
},
|
||||
})}
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Enter Company Email"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("phone", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
label="Phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
placeholder="Enter Company Phone"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.phone.message}</p>
|
||||
)}
|
||||
<Select
|
||||
{...register("type", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
name="type"
|
||||
label="Company type"
|
||||
items={[
|
||||
{ value: "private", label: "Private" },
|
||||
{ value: "public", label: "Public" },
|
||||
{ value: "government", label: "Government" },
|
||||
{ value: "ngo", label: "NGO" },
|
||||
{ value: "startup", label: "Startup" },
|
||||
]}
|
||||
defaultValue="Private"
|
||||
prefixIcon={<GlobeIcon />}
|
||||
/>
|
||||
{errors.type && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.type.message}</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("registration_number", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 5, message: FormErrorMessages.minLength(5) },
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: FormErrorMessages.maxLength(50),
|
||||
},
|
||||
})}
|
||||
label="Registration Number"
|
||||
name="registration_number"
|
||||
type="text"
|
||||
placeholder="Enter Registration Number"
|
||||
/>
|
||||
{errors.registration_number && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.registration_number.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("tax_id", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 5, message: FormErrorMessages.minLength(5) },
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: FormErrorMessages.maxLength(50),
|
||||
},
|
||||
})}
|
||||
label="Tax id"
|
||||
name="tax_id"
|
||||
type="text"
|
||||
placeholder="Enter Tax id"
|
||||
/>
|
||||
{errors.tax_id && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.tax_id.message}</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("industry", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="Industry"
|
||||
name="industry"
|
||||
type="text"
|
||||
placeholder="Enter Industry"
|
||||
/>
|
||||
{errors.industry && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.industry.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("website", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="Website"
|
||||
name="website"
|
||||
type="url"
|
||||
placeholder="Enter Company Website"
|
||||
/>
|
||||
{errors.website && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.website.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection
|
||||
title="Contact Person"
|
||||
className="grid grid-cols-1 gap-5"
|
||||
>
|
||||
<InputGroup
|
||||
{...register("contact_person.full_name", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="Full name"
|
||||
name="contact_person.full_name"
|
||||
placeholder="Enter Full name"
|
||||
type="text"
|
||||
/>
|
||||
{errors.contact_person?.full_name && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.contact_person.full_name.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("contact_person.position", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="Position"
|
||||
name="contact_person.position"
|
||||
placeholder="Enter Content Person Position"
|
||||
type="text"
|
||||
/>
|
||||
{errors.contact_person?.position && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.contact_person.position.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("contact_person.email", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
pattern: {
|
||||
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: FormErrorMessages.invalidEmail,
|
||||
},
|
||||
})}
|
||||
label="Email"
|
||||
name="contact_person.email"
|
||||
placeholder="Enter Contact Person Email"
|
||||
type="email"
|
||||
/>
|
||||
{errors.contact_person?.email && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.contact_person.email.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("contact_person.phone", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
label="Phone"
|
||||
name="contact_person.phone"
|
||||
placeholder="Enter Contact Person Phone"
|
||||
type="tel"
|
||||
/>
|
||||
{errors.contact_person?.phone && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.contact_person.phone.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("contact_person.mobile", {
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
label="Mobile"
|
||||
name="contact_person.mobile"
|
||||
placeholder="Enter Contact Person Mobile"
|
||||
type="tel"
|
||||
/>
|
||||
{errors.contact_person?.mobile && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.contact_person.mobile.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Settings" className="grid grid-cols-1 gap-5">
|
||||
<Select
|
||||
{...register("language")}
|
||||
name="language"
|
||||
label="Language"
|
||||
items={[
|
||||
{ value: "en", label: "EN" },
|
||||
{ value: "ar", label: "AR" },
|
||||
{ value: "fr", label: "FR" },
|
||||
{ value: "es", label: "ES" },
|
||||
{ value: "de", label: "DE" },
|
||||
{ value: "zh", label: "ZH" },
|
||||
{ value: "jEna", label: "jEna" },
|
||||
{ value: "ko", label: "KO" },
|
||||
]}
|
||||
defaultValue="en"
|
||||
prefixIcon={<GlobeIcon />}
|
||||
/>
|
||||
{errors.language && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.language.message}
|
||||
</p>
|
||||
)}
|
||||
<Select
|
||||
{...register("currency")}
|
||||
name="currency"
|
||||
label="Currency"
|
||||
items={[
|
||||
{ label: "USD", value: "USD" },
|
||||
{ label: "EUR", value: "EUR" },
|
||||
{ label: "GBP", value: "GBP" },
|
||||
{ label: "JPY", value: "JPY" },
|
||||
{ label: "CAD", value: "CAD" },
|
||||
{ label: "AUD", value: "AUD" },
|
||||
{ label: "CHF", value: "CHF" },
|
||||
{ label: "CNY", value: "CNY" },
|
||||
{ label: "INR", value: "INR" },
|
||||
{ label: "BRL", value: "BRL" },
|
||||
]}
|
||||
defaultValue="en"
|
||||
prefixIcon={<MoneyIcon />}
|
||||
/>
|
||||
{errors.currency && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.currency.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("timezone", {
|
||||
minLength: { value: 3, message: FormErrorMessages.minLength(3) },
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: FormErrorMessages.maxLength(50),
|
||||
},
|
||||
})}
|
||||
label="Timezone"
|
||||
name="timezone"
|
||||
type="text"
|
||||
placeholder="Enter Timezone"
|
||||
/>
|
||||
{errors.timezone && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.timezone.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
title="Business Information"
|
||||
className="grid grid-cols-1 gap-5"
|
||||
>
|
||||
<InputGroup
|
||||
{...register("employee_count", {
|
||||
min: { value: 1, message: FormErrorMessages.min(1) },
|
||||
max: { value: 100000, message: FormErrorMessages.max(100000) },
|
||||
})}
|
||||
label="Employee Count"
|
||||
name="employee_count"
|
||||
type="number"
|
||||
placeholder="Enter Employee Count"
|
||||
/>
|
||||
{errors.employee_count && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.employee_count.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("annual_revenue", {
|
||||
min: { value: 0, message: FormErrorMessages.min(0) },
|
||||
max: {
|
||||
value: 999999999999,
|
||||
message: FormErrorMessages.max(999999999999),
|
||||
},
|
||||
})}
|
||||
label="Annual Revenue"
|
||||
name="annual_revenue"
|
||||
type="number"
|
||||
placeholder="Enter Annual Revenue"
|
||||
/>
|
||||
{errors.annual_revenue && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.annual_revenue.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("founded_year", {
|
||||
min: { value: 1800, message: FormErrorMessages.min(1800) },
|
||||
max: {
|
||||
value: 2100,
|
||||
message: FormErrorMessages.max(2100),
|
||||
},
|
||||
})}
|
||||
label="Founded Year"
|
||||
name="founded_year"
|
||||
type="number"
|
||||
placeholder="Enter Founded Year"
|
||||
/>
|
||||
{errors.founded_year && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.founded_year.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Address" className="grid grid-cols-1 gap-5">
|
||||
<InputGroup
|
||||
{...register("address.street", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 5,
|
||||
message: FormErrorMessages.minLength(5),
|
||||
},
|
||||
maxLength: {
|
||||
value: 200,
|
||||
message: FormErrorMessages.maxLength(200),
|
||||
},
|
||||
})}
|
||||
label="Street"
|
||||
name="address.street"
|
||||
type="text"
|
||||
placeholder="Enter Street"
|
||||
/>
|
||||
{errors.address?.street && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.address.street.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("address.city", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="City"
|
||||
name="address.city"
|
||||
type="text"
|
||||
placeholder="Enter City"
|
||||
/>
|
||||
{errors.address?.city && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.address.city.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("address.state", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="State"
|
||||
name="address.state"
|
||||
type="text"
|
||||
placeholder="Enter State"
|
||||
/>
|
||||
{errors.address?.state && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.address.state.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("address.postal_code", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: FormErrorMessages.minLength(3),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
label="Postal code"
|
||||
name="address.postal_code"
|
||||
type="text"
|
||||
placeholder="Enter Postal code"
|
||||
/>
|
||||
{errors.address?.postal_code && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.address.postal_code.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("address.country", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
label="Country"
|
||||
name="address.country"
|
||||
type="text"
|
||||
placeholder="Enter Country"
|
||||
/>
|
||||
{errors.address?.country && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.address.country.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Tags" className="grid grid-cols-1 gap-5">
|
||||
<InputGroup
|
||||
{...register("tags.keywords")}
|
||||
label="Keywords"
|
||||
name="tags.keywords"
|
||||
type="text"
|
||||
placeholder="Enter Tag Keywords"
|
||||
/>
|
||||
{errors.tags?.keywords && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.keywords.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("tags.categories")}
|
||||
label="Categories"
|
||||
name="tags.categories"
|
||||
type="text"
|
||||
placeholder="Enter Tag Categories"
|
||||
/>
|
||||
{errors.tags?.categories && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.categories.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("tags.specializations")}
|
||||
label="Specializations"
|
||||
name="tags.specializations"
|
||||
type="text"
|
||||
placeholder="Enter Tag Specializations"
|
||||
/>
|
||||
{errors.tags?.specializations && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.specializations.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("tags.certifications")}
|
||||
label="Certifications"
|
||||
name="tags.certifications"
|
||||
type="text"
|
||||
placeholder="Enter Tag Certifications"
|
||||
/>
|
||||
{errors.tags?.certifications && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.certifications.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("tags.cpv_codes")}
|
||||
label="CPV Code"
|
||||
name="tags.cpv_codes"
|
||||
type="text"
|
||||
placeholder="Enter Tag CPV Code"
|
||||
/>
|
||||
{errors.tags?.cpv_codes && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.tags.cpv_codes.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex gap-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={router.back}
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Opt out
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateCompany;
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
import { useCreateCompany, useUpdateCompany } from "@/hooks/queries";
|
||||
import { ICreateCompanyCredentials } from "@/lib/api";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: ICreateCompanyCredentials;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
|
||||
const isMutating = useIsMutating();
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<ICreateCompanyCredentials>({ mode: "onChange", defaultValues });
|
||||
const router = useRouter();
|
||||
const { mutate: createCompany, isPending: isCreating } = useCreateCompany();
|
||||
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
|
||||
id as string,
|
||||
);
|
||||
const onSubmit: SubmitHandler<ICreateCompanyCredentials> = (data) => {
|
||||
const formattedData = {
|
||||
...data,
|
||||
founded_year: data.founded_year ? +data.founded_year : 0,
|
||||
employee_count: data.employee_count ? +data.employee_count : 0,
|
||||
annual_revenue: data.annual_revenue ? +data.annual_revenue : 0,
|
||||
tags: {
|
||||
keywords: data.tags?.keywords
|
||||
? (data.tags.keywords as unknown as string)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
: [],
|
||||
categories: data.tags?.categories
|
||||
? (data.tags.categories as unknown as string)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
: [],
|
||||
specializations: data.tags?.specializations
|
||||
? (data.tags.specializations as unknown as string)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
: [],
|
||||
certifications: data.tags?.certifications
|
||||
? (data.tags.certifications as unknown as string)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
: [],
|
||||
cpv_codes: data.tags?.cpv_codes
|
||||
? (data.tags.cpv_codes as unknown as string)
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
: [],
|
||||
},
|
||||
};
|
||||
if (editMode) {
|
||||
updateCompany({
|
||||
id: id as string,
|
||||
credentials: formattedData as ICreateCompanyCredentials,
|
||||
});
|
||||
} else {
|
||||
createCompany(formattedData as ICreateCompanyCredentials);
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
reset();
|
||||
router.back();
|
||||
};
|
||||
return {
|
||||
errors,
|
||||
register,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
handleCancel,
|
||||
isMutating,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
router
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateCompanyPresenter;
|
||||
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { CreateCustomerCredentials } from "@/lib/api";
|
||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||
import { ShowcaseSection } from "../../Layouts/showcase-section";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: CreateCustomerCredentials;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
const { errors, handleSubmit, register, onSubmit, companies, router } =
|
||||
useCreateCustomerPresenter({ defaultValues, editMode, id });
|
||||
|
||||
return (
|
||||
<form className="grid grid-cols-1 gap-9" onSubmit={handleSubmit(onSubmit)}>
|
||||
<ShowcaseSection
|
||||
title="Create Customer"
|
||||
className="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
{...register("username", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
minLength: { value: 3, message: FormErrorMessages.minLength(3) },
|
||||
maxLength: {
|
||||
value: 30,
|
||||
message: FormErrorMessages.maxLength(30),
|
||||
},
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9]+$/,
|
||||
message: FormErrorMessages.alphaNumeric,
|
||||
},
|
||||
})}
|
||||
name="username"
|
||||
label="Username"
|
||||
type="text"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.username.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<MultiSelect
|
||||
label="Select companies"
|
||||
items={
|
||||
companies
|
||||
? companies?.map((item) => ({
|
||||
value: item.id,
|
||||
text: item.name,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
placeholder="Choose your interests..."
|
||||
{...register("company_ids", {
|
||||
required: "Please select at least one interest",
|
||||
})}
|
||||
errors={errors}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
{...register("email", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
pattern: {
|
||||
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: FormErrorMessages.invalidEmail,
|
||||
},
|
||||
})}
|
||||
name="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="Enter email"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
{...register("password", {
|
||||
required: { value: true, message: FormErrorMessages.required },
|
||||
minLength: { value: 8, message: FormErrorMessages.minLength(8) },
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: FormErrorMessages.maxLength(128),
|
||||
},
|
||||
})}
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
{...register("full_name", {
|
||||
minLength: { value: 2, message: FormErrorMessages.minLength(2) },
|
||||
maxLength: {
|
||||
value: 100,
|
||||
message: FormErrorMessages.maxLength(100),
|
||||
},
|
||||
})}
|
||||
name="full_name"
|
||||
label="Full name"
|
||||
type="full_name"
|
||||
placeholder="Enter full name"
|
||||
/>
|
||||
{errors.full_name && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.full_name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
{...register("phone", {
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
name="phone"
|
||||
label="Phone"
|
||||
type="tel"
|
||||
placeholder="Enter phone"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.phone.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<InputGroup
|
||||
{...register("mobile", {
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: FormErrorMessages.maxLength(20),
|
||||
},
|
||||
})}
|
||||
name="mobile"
|
||||
label="Mobile"
|
||||
type="tel"
|
||||
placeholder="Enter mobile"
|
||||
/>
|
||||
{errors.mobile && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.mobile.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex gap-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={router.back}
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
Opt out
|
||||
</button>
|
||||
</div>
|
||||
</ShowcaseSection>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateCustomer;
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useCompanyFullList,
|
||||
useCreateCustomer,
|
||||
useUpdateCustomer,
|
||||
} from "@/hooks/queries";
|
||||
import { CreateCustomerCredentials } from "@/lib/api";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
interface IProps {
|
||||
editMode?: boolean;
|
||||
defaultValues?: CreateCustomerCredentials;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const useCreateCustomerPresenter = ({
|
||||
defaultValues,
|
||||
editMode,
|
||||
id,
|
||||
}: IProps) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<CreateCustomerCredentials>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
});
|
||||
const router = useRouter();
|
||||
const { mutate: createCustomer, isPending: isCreating } = useCreateCustomer();
|
||||
const { mutate: updateCustomer, isPending: isUpdating } = useUpdateCustomer(
|
||||
id as string,
|
||||
);
|
||||
const { data: companies } = useCompanyFullList();
|
||||
const onSubmit: SubmitHandler<CreateCustomerCredentials> = (data) => {
|
||||
if (editMode) {
|
||||
updateCustomer({
|
||||
id: id as string,
|
||||
credentials: data,
|
||||
});
|
||||
} else {
|
||||
console.log(data.company_ids);
|
||||
|
||||
createCustomer({
|
||||
...data,
|
||||
employee_count: data.employee_count ? +data.employee_count : undefined,
|
||||
annual_revenue: data.annual_revenue ? +data.annual_revenue : undefined,
|
||||
founded_year: data.founded_year ? +data.founded_year : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
handleSubmit,
|
||||
register,
|
||||
errors,
|
||||
onSubmit,
|
||||
companies: companies?.data.companies,
|
||||
router,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateCustomerPresenter;
|
||||
@@ -0,0 +1,15 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<Image
|
||||
src="/images/main-logo.svg"
|
||||
width={174}
|
||||
height={32}
|
||||
alt="NextAdmin logo"
|
||||
role="presentation"
|
||||
quality={100}
|
||||
className="dark:invert"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronUpIcon } from "@/assets/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Dropdown, DropdownContent, DropdownTrigger } from "./ui/dropdown";
|
||||
|
||||
type PropsType<TItem> = {
|
||||
defaultValue?: TItem;
|
||||
items?: TItem[];
|
||||
sectionKey: string;
|
||||
minimal?: boolean;
|
||||
};
|
||||
|
||||
const PARAM_KEY = "selected_time_frame";
|
||||
|
||||
export function PeriodPicker<TItem extends string>({
|
||||
defaultValue,
|
||||
sectionKey,
|
||||
items,
|
||||
minimal,
|
||||
}: PropsType<TItem>) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={isOpen} setIsOpen={setIsOpen}>
|
||||
<DropdownTrigger
|
||||
className={cn(
|
||||
"flex h-8 w-full items-center justify-between gap-x-1 rounded-md border border-[#E8E8E8] bg-white px-3 py-2 text-sm font-medium text-dark-5 outline-none ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-neutral-500 dark:border-dark-3 dark:bg-dark-2 dark:text-white dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[placeholder]:text-neutral-400 [&>span]:line-clamp-1 [&[data-state='open']>svg]:rotate-0",
|
||||
minimal &&
|
||||
"border-none bg-transparent p-0 text-dark dark:bg-transparent dark:text-white",
|
||||
)}
|
||||
>
|
||||
<span className="capitalize">{defaultValue || "Time Period"}</span>
|
||||
|
||||
<ChevronUpIcon className="size-4 rotate-180 transition-transform" />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent
|
||||
align="end"
|
||||
className="min-w-[7rem] overflow-hidden rounded-lg border border-[#E8E8E8] bg-white p-1 font-medium text-dark-5 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-dark-3 dark:bg-dark-2 dark:text-current"
|
||||
>
|
||||
<ul>
|
||||
{(items || ["monthly", "yearly"]).map((item) => (
|
||||
<li key={crypto.randomUUID()}>
|
||||
<button
|
||||
className="flex w-full select-none items-center truncate rounded-md px-3 py-2 text-sm capitalize outline-none hover:bg-[#F9FAFB] hover:text-dark-3 dark:hover:bg-[#FFFFFF1A] dark:hover:text-white"
|
||||
onClick={() => {
|
||||
const queryString = createQueryString({
|
||||
sectionKey,
|
||||
value: item,
|
||||
selectedTimeFrame: searchParams.get(PARAM_KEY),
|
||||
});
|
||||
|
||||
router.push(pathname + queryString, {
|
||||
scroll: false,
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const createQueryString = (props: {
|
||||
sectionKey: string;
|
||||
value: string;
|
||||
selectedTimeFrame: string | null;
|
||||
}) => {
|
||||
const paramsValue = `${props.sectionKey}:${props.value}`;
|
||||
|
||||
if (!props.selectedTimeFrame) {
|
||||
return `?${PARAM_KEY}=${paramsValue}`;
|
||||
}
|
||||
|
||||
const newSearchParams = props.selectedTimeFrame
|
||||
.split(",")
|
||||
.filter((value) => !value.includes(props.sectionKey))
|
||||
.join(",");
|
||||
|
||||
if (!newSearchParams) {
|
||||
return `?${PARAM_KEY}=${paramsValue}`;
|
||||
}
|
||||
|
||||
return `?${PARAM_KEY}=${newSearchParams},${paramsValue}`;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IconProps } from "@/types/icon-props";
|
||||
|
||||
export function AlertWarningIcon(props: IconProps) {
|
||||
return (
|
||||
<svg width={36} height={36} viewBox="0 0 36 36" fill="none" {...props}>
|
||||
<rect width={36} height={36} rx={8} fill="#FBBF24" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.5 16.68c0-2.664 0-3.997.315-4.445.314-.448 1.567-.877 4.072-1.734l.478-.164c1.306-.447 1.959-.67 2.635-.67.676 0 1.33.223 2.635.67l.478.164c2.505.857 3.758 1.286 4.072 1.734.315.448.315 1.78.315 4.445v1.313c0 4.698-3.532 6.978-5.749 7.946-.601.263-.902.394-1.751.394-.85 0-1.15-.131-1.751-.394-2.216-.968-5.749-3.248-5.749-7.946V16.68zm7.5-2.639c.345 0 .625.28.625.625V18a.625.625 0 11-1.25 0v-3.334c0-.345.28-.625.625-.625zm0 7.292a.833.833 0 100-1.666.833.833 0 000 1.666z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertSuccessIcon(props: IconProps) {
|
||||
return (
|
||||
<svg width={32} height={32} viewBox="0 0 32 32" fill="none" {...props}>
|
||||
<rect width={32} height={32} rx={6} fill="#22AD5C" />
|
||||
<path
|
||||
d="M23.298 10.827l-.011-.015-.013-.014a.87.87 0 00-1.308-.004l-8.047 8.66-3.862-4.167a.87.87 0 00-1.309.003.976.976 0 000 1.3h0l.005.005 4.114 4.44c.277.305.662.465 1.029.465.396 0 .756-.165 1.028-.465l8.292-8.923a.98.98 0 00.082-1.285z"
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertErrorIcon(props: IconProps) {
|
||||
return (
|
||||
<svg width={32} height={32} viewBox="0 0 32 32" fill="none" {...props}>
|
||||
<rect width={32} height={32} rx={6} fill="#F56060" />
|
||||
<path
|
||||
d="M12.796 11.723h0l3.698 3.707 3.71-3.692.354.353-.353-.354h0a.756.756 0 011.072 0 .755.755 0 010 1.073h-.001l-3.71 3.693 3.696 3.705s0 0 0 0a.755.755 0 010 1.073l-.353-.354.353.354a.768.768 0 01-.529.219c-.19 0-.39-.08-.529-.22l.354-.353-.354.354-3.71-3.707-3.698 3.706h0a.77.77 0 01-.53.22c-.188 0-.39-.08-.528-.22l.353-.353-.353.354a.755.755 0 010-1.073l3.684-3.705-3.699-3.707s0 0 0 0a.755.755 0 010-1.073.756.756 0 011.073 0z"
|
||||
fill="#fff"
|
||||
stroke="#fff"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva } from "class-variance-authority";
|
||||
import React from "react";
|
||||
import { AlertErrorIcon, AlertSuccessIcon, AlertWarningIcon } from "./icons";
|
||||
|
||||
const alertVariants = cva(
|
||||
"flex gap-5 w-full rounded-[10px] border-l-6 px-7 py-8 dark:bg-opacity-30 md:p-9",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
success: "border-green bg-green-light-7 dark:bg-[#1B1B24]",
|
||||
warning: "border-[#FFB800] bg-[#FEF5DE] dark:bg-[#1B1B24]",
|
||||
error: "border-red-light bg-red-light-5 dark:bg-[#1B1B24]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "error",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const icons = {
|
||||
error: AlertErrorIcon,
|
||||
success: AlertSuccessIcon,
|
||||
warning: AlertWarningIcon,
|
||||
};
|
||||
|
||||
type AlertProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
variant: "error" | "success" | "warning";
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const Alert = ({
|
||||
className,
|
||||
variant,
|
||||
title,
|
||||
description,
|
||||
...props
|
||||
}: AlertProps) => {
|
||||
const IconComponent = icons[variant];
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
<IconComponent />
|
||||
|
||||
<div className="w-full">
|
||||
<h5
|
||||
className={cn("mb-4 font-bold leading-[22px]", {
|
||||
"text-[#004434] dark:text-[#34D399]": variant === "success",
|
||||
"text-[#9D5425]": variant === "warning",
|
||||
"text-[#BC1C21]": variant === "error",
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</h5>
|
||||
|
||||
<div
|
||||
className={cn({
|
||||
"text-[#637381]": variant === "success",
|
||||
"text-[#D0915C]": variant == "warning",
|
||||
"text-[#CD5D5D]": variant === "error",
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Alert, type AlertProps };
|
||||
@@ -0,0 +1,60 @@
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2.5 text-center font-medium hover:bg-opacity-90 font-medium transition focus:outline-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-primary text-white",
|
||||
green: "bg-green text-white",
|
||||
dark: "bg-dark text-white dark:bg-white/10",
|
||||
outlinePrimary:
|
||||
"border border-primary hover:bg-primary/10 text-primary",
|
||||
outlineGreen: "border border-green hover:bg-green/10 text-green",
|
||||
outlineDark:
|
||||
"border border-dark hover:bg-dark/10 text-dark dark:hover:bg-white/10 dark:border-white/25 dark:text-white",
|
||||
},
|
||||
shape: {
|
||||
default: "",
|
||||
rounded: "rounded-[5px]",
|
||||
full: "rounded-full",
|
||||
},
|
||||
size: {
|
||||
default: "py-3.5 px-10 py-3.5 lg:px-8 xl:px-10",
|
||||
small: "py-[11px] px-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
shape: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type ButtonProps = HTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Button({
|
||||
label,
|
||||
icon,
|
||||
variant,
|
||||
shape,
|
||||
size,
|
||||
className,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={buttonVariants({ variant, shape, size, className })}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span>{icon}</span>}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
const ConfirmationModalPropsSchema = z.object({
|
||||
isOpen: z.boolean(),
|
||||
title: z.string().min(1, "Title is required"),
|
||||
message: z.string().min(1, "Message is required"),
|
||||
confirmText: z.string().optional().default("Confirm"),
|
||||
cancelText: z.string().optional().default("Cancel"),
|
||||
onConfirm: z.function(),
|
||||
onCancel: z.function(),
|
||||
variant: z
|
||||
.enum(["default", "danger", "warning"])
|
||||
.optional()
|
||||
.default("default"),
|
||||
size: z.enum(["sm", "md", "lg"]).optional().default("md"),
|
||||
});
|
||||
|
||||
export type ConfirmationModalProps = z.infer<
|
||||
typeof ConfirmationModalPropsSchema
|
||||
>;
|
||||
|
||||
const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
const validatedProps = ConfirmationModalPropsSchema.parse(props);
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
variant,
|
||||
size,
|
||||
} = validatedProps;
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isOpen) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [isOpen, onCancel]);
|
||||
|
||||
if (!mounted || !isOpen) return null;
|
||||
|
||||
const variantStyles = {
|
||||
default: {
|
||||
confirmBtn: "bg-primary hover:bg-primary/90 focus:ring-primary",
|
||||
icon: "🔔",
|
||||
iconBg: "bg-blue-100",
|
||||
},
|
||||
danger: {
|
||||
confirmBtn: "bg-red-light hover:bg-red-light/90 focus:ring-red-light",
|
||||
icon: "⚠️",
|
||||
iconBg: "bg-red-light/20",
|
||||
},
|
||||
warning: {
|
||||
confirmBtn:
|
||||
"bg-yellow-dark hover:bg-yellow-dark/90 focus:ring-yellow-dark",
|
||||
icon: "⚠️",
|
||||
iconBg: "bg-yellow-light/20",
|
||||
},
|
||||
};
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "max-w-sm",
|
||||
md: "max-w-md",
|
||||
lg: "max-w-lg",
|
||||
};
|
||||
|
||||
const currentVariant = variantStyles[variant];
|
||||
const currentSize = sizeStyles[size];
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 bg-dark/70 bg-opacity-75 transition-opacity"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div
|
||||
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full ${currentSize} dark:bg-dark-2`}
|
||||
>
|
||||
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4 dark:bg-dark-2">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div
|
||||
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full ${currentVariant.iconBg} sm:mx-0 sm:h-10 sm:w-10`}
|
||||
>
|
||||
<span className="text-xl">{currentVariant.icon}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3
|
||||
className="text-base font-semibold leading-6 text-dark dark:text-white"
|
||||
id="modal-title"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-dark-5 dark:text-dark-6">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-dark-2 dark:border-t dark:border-stroke-dark">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${currentVariant.confirmBtn} dark:ring-offset-dark-2`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 sm:mt-0 sm:w-auto dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
export default ConfirmationModal;
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useClickOutside } from "@/hooks/use-click-outside";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SetStateActionType } from "@/types/set-state-action-type";
|
||||
import {
|
||||
createContext,
|
||||
type PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
type DropdownContextType = {
|
||||
isOpen: boolean;
|
||||
handleOpen: () => void;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const DropdownContext = createContext<DropdownContextType | null>(null);
|
||||
|
||||
function useDropdownContext() {
|
||||
const context = useContext(DropdownContext);
|
||||
if (!context) {
|
||||
throw new Error("useDropdownContext must be used within a Dropdown");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
type DropdownProps = {
|
||||
children: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
setIsOpen: SetStateActionType<boolean>;
|
||||
};
|
||||
|
||||
export function Dropdown({ children, isOpen, setIsOpen }: DropdownProps) {
|
||||
const triggerRef = useRef<HTMLElement>(null);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
triggerRef.current = document.activeElement as HTMLElement;
|
||||
|
||||
document.body.style.pointerEvents = "none";
|
||||
} else {
|
||||
document.body.style.removeProperty("pointer-events");
|
||||
|
||||
setTimeout(() => {
|
||||
triggerRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownContext.Provider value={{ isOpen, handleOpen, handleClose }}>
|
||||
<div className="relative" onKeyDown={handleKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
type DropdownContentProps = {
|
||||
align?: "start" | "end" | "center";
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function DropdownContent({
|
||||
children,
|
||||
align = "center",
|
||||
className,
|
||||
}: DropdownContentProps) {
|
||||
const { isOpen, handleClose } = useDropdownContext();
|
||||
|
||||
const contentRef = useClickOutside<HTMLDivElement>(() => {
|
||||
if (isOpen) handleClose();
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
className={cn(
|
||||
"fade-in-0 zoom-in-95 pointer-events-auto absolute z-99 mt-2 min-w-[8rem] origin-top-right rounded-lg",
|
||||
{
|
||||
"animate-in right-0": align === "end",
|
||||
"left-0": align === "start",
|
||||
"left-1/2 -translate-x-1/2": align === "center",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DropdownTriggerProps = React.HTMLAttributes<HTMLButtonElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function DropdownTrigger({ children, className }: DropdownTriggerProps) {
|
||||
const { handleOpen, isOpen } = useDropdownContext();
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={handleOpen}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="menu"
|
||||
data-state={isOpen ? "open" : "closed"}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function DropdownClose({ children }: PropsWithChildren) {
|
||||
const { handleClose } = useDropdownContext();
|
||||
|
||||
return <div onClick={handleClose}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface IProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ShadowBox = ({ children }: IProps) => {
|
||||
return (
|
||||
<div className="mb-4 mt-2 rounded-3xl border p-4 shadow-xl dark:!border-gray-700 dark:shadow-gray-700">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShadowBox;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"animate-pulse rounded-md bg-neutral-100 dark:bg-dark-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
|
||||
export function Table({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableElement>) {
|
||||
return (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return <thead className={cn("[&_tr]:border-b", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableBody({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return (
|
||||
<tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export function TableFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return (
|
||||
<tfoot
|
||||
className={cn(
|
||||
"border-t bg-neutral-100/50 font-medium dark:bg-neutral-800/50 [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRow({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableRowElement>) {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:border-dark-3 dark:hover:bg-dark-2 dark:data-[state=selected]:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableHead({
|
||||
className,
|
||||
...props
|
||||
}: React.ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableCell({
|
||||
className,
|
||||
...props
|
||||
}: React.TdHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<td
|
||||
className={cn(
|
||||
"p-4 align-middle [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user