Merge pull request 'table-filter' (#46) from table-filter into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/46 Reviewed-by: Sina Nakhostin <s.nakhostin@ravanertebat.com>
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
Generated
+9
-9
@@ -11,7 +11,7 @@
|
||||
"@tanstack/react-query": "^5.85.6",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react-paginate": "^7.1.4",
|
||||
"apexcharts": "^4.5.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -26,7 +26,7 @@
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^3.7.15",
|
||||
"react": "19.0.0",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-apexcharts": "^1.8.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
@@ -2353,9 +2353,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/apexcharts": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.5.0.tgz",
|
||||
"integrity": "sha512-E7ZkrVqPNBUWy/Rmg8DEIqHNBmElzICE/oxOX5Ekvs2ICQUOK/VkEkMH09JGJu+O/EA0NL31hxlmF+wrwrSLaQ==",
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/apexcharts/-/apexcharts-4.7.0.tgz",
|
||||
"integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.draggable.js": "^3.0.4",
|
||||
@@ -6200,10 +6200,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-apexcharts": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.7.0.tgz",
|
||||
"integrity": "sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==",
|
||||
"license": "MIT",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-apexcharts/-/react-apexcharts-1.8.0.tgz",
|
||||
"integrity": "sha512-MuEp56gc0NMO2UUgY94fxQzoBE4XEjmcCha4xYY0vJdRrc1yfFFZE4QrCekOt2wcS3nibghzca/q/CbgkAgN5w==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
"@tanstack/react-query": "^5.85.6",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react-paginate": "^7.1.4",
|
||||
"apexcharts": "^4.5.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^3.7.15",
|
||||
"react": "19.0.0",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-apexcharts": "^1.8.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import ContactUsTable from "@/components/Tables/contact-us";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
|
||||
const ContactUsPage = () => {
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "/contact-us",
|
||||
name: "Contact us",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<ContactUsTable />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactUsPage;
|
||||
@@ -113,9 +113,9 @@ export default function FormElementsPage() {
|
||||
title="Checkbox and radio"
|
||||
className="space-y-5.5 !p-6.5"
|
||||
>
|
||||
<Checkbox label="Checkbox Text" />
|
||||
{/* <Checkbox label="Checkbox Text" />
|
||||
<Checkbox label="Checkbox Text" withIcon="check" />
|
||||
<Checkbox label="Checkbox Text" withIcon="x" />
|
||||
<Checkbox label="Checkbox Text" withIcon="x" /> */}
|
||||
<RadioInput label="Checkbox Text" />
|
||||
<RadioInput label="Checkbox Text" variant="circle" />
|
||||
</ShowcaseSection>
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SignInForm() {
|
||||
/> */}
|
||||
|
||||
<div className="mb-5.5 mt-5 flex items-center justify-between">
|
||||
<Checkbox label="Remember me" minimal withBg withIcon="check" />
|
||||
{/* <Checkbox label="Remember me" minimal withBg withIcon="check" /> */}
|
||||
|
||||
<Link href="#" className="text-body-sm text-primary hover:underline">
|
||||
Forgot password?
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import SectionStep from "./SectionStep";
|
||||
import useBenefitsStepPresenter, {
|
||||
IBenefitsStepFields,
|
||||
} from "./useBenefitsStepPresenter";
|
||||
|
||||
interface BenefitsStepProps {
|
||||
initialData?: IBenefitsStepFields;
|
||||
}
|
||||
|
||||
const BenefitsStep = forwardRef<unknown, BenefitsStepProps>(({ initialData }, ref) => {
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
register,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
getValues,
|
||||
} = useBenefitsStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<SectionStep<IBenefitsStepFields>
|
||||
title="Benefits"
|
||||
fields={fields}
|
||||
handleSubmit={handleSubmit}
|
||||
onSubmit={onSubmit}
|
||||
register={register}
|
||||
errors={errors}
|
||||
append={append}
|
||||
remove={remove}
|
||||
control={control}
|
||||
fieldName="cards"
|
||||
defaultValues={{ icon: "", title: "", description: "" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
BenefitsStep.displayName = "BenefitsStep";
|
||||
|
||||
export default BenefitsStep;
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import SectionStep from "./SectionStep";
|
||||
import useChallengesStepPresenter, {
|
||||
IChallengesStepFields,
|
||||
} from "./useChallengesStepPresenter";
|
||||
|
||||
interface ChallengesStepProps {
|
||||
initialData?: IChallengesStepFields;
|
||||
}
|
||||
|
||||
const ChallengesStep = forwardRef<unknown, ChallengesStepProps>(
|
||||
({ initialData }, ref) => {
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
register,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
getValues,
|
||||
} = useChallengesStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<SectionStep<IChallengesStepFields>
|
||||
title="Challenges"
|
||||
fields={fields}
|
||||
handleSubmit={handleSubmit}
|
||||
onSubmit={onSubmit}
|
||||
register={register}
|
||||
errors={errors}
|
||||
append={append}
|
||||
remove={remove}
|
||||
control={control}
|
||||
fieldName="cards"
|
||||
defaultValues={{ icon: "", title: "", description: "" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ChallengesStep.displayName = "ChallengesStep";
|
||||
|
||||
export default ChallengesStep;
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import type { ApexOptions } from "apexcharts";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
type PropsType = {
|
||||
series: { x: string; y: number }[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
const Chart = dynamic(() => import("react-apexcharts"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function ChartPreview({ series, title }: PropsType) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const options: ApexOptions = {
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
colors: ["#5750F1"],
|
||||
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: title,
|
||||
data: series,
|
||||
},
|
||||
]}
|
||||
type="area"
|
||||
height={310}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
|
||||
import { TrashIcon } from "@/assets/icons";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import { ChartPreview } from "./ChartPreview";
|
||||
import useChartStepPresenter from "./useChartStepPresenter";
|
||||
|
||||
interface ChartStepProps {
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const ChartStep = forwardRef<unknown, ChartStepProps>(
|
||||
({ initialData }, ref) => {
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
register,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
watch,
|
||||
getValues,
|
||||
} = useChartStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
const chartTitle = watch("title");
|
||||
const chartData = watch("data");
|
||||
|
||||
return (
|
||||
<form
|
||||
className="items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7 sm:grid-cols-2"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
title="Chart Information"
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<InputGroup
|
||||
{...register("title", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
label="Chart Title"
|
||||
name="title"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Chart Title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.title.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("missedAmount", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
label="Missed Amount"
|
||||
name="missedAmount"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Missed Amount"
|
||||
/>
|
||||
{errors.missedAmount && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.missedAmount.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Chart Preview">
|
||||
<ChartPreview
|
||||
series={
|
||||
chartData
|
||||
?.filter((p) => p.key && p.value)
|
||||
.map((p) => ({ x: p.key, y: p.value })) || []
|
||||
}
|
||||
title={chartTitle}
|
||||
/>
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Chart Points" className="flex flex-col gap-5">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center gap-4">
|
||||
<InputGroup
|
||||
{...register(`data.${index}.key`, {
|
||||
required: {
|
||||
message: FormErrorMessages.required,
|
||||
value: true,
|
||||
},
|
||||
})}
|
||||
label="Key"
|
||||
name={`data.${index}.key`}
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter key"
|
||||
/>
|
||||
<InputGroup
|
||||
{...register(`data.${index}.value`, {
|
||||
required: {
|
||||
message: FormErrorMessages.required,
|
||||
value: true,
|
||||
},
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
label="Value"
|
||||
name={`data.${index}.value`}
|
||||
required
|
||||
type="number"
|
||||
placeholder="Enter value"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 flex w-fit items-center justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
onClick={() => append({ key: "", value: 0 })}
|
||||
>
|
||||
Add point
|
||||
</button>
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
<div className="flex flex-col gap-9"></div>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ChartStep.displayName = "ChartStep";
|
||||
|
||||
export default ChartStep;
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
import { Checkbox } from "@/components/FormElements/checkbox";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import useContactsStepPresenter from "./useContactsStepPresenter";
|
||||
|
||||
interface ContactsStepProps {
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const ContactsStep = forwardRef<unknown, ContactsStepProps>(
|
||||
({ initialData }, ref) => {
|
||||
const { errors, handleSubmit, onSubmit, register, fields, getValues } =
|
||||
useContactsStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<ShowcaseSection
|
||||
title="Contacts Section"
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<InputGroup
|
||||
{...register("title", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
label="Title"
|
||||
name="title"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.title.message as string}
|
||||
</p>
|
||||
)}
|
||||
<TextAreaGroup
|
||||
label="Description"
|
||||
{...register("description")}
|
||||
name="description"
|
||||
placeholder="Enter Description"
|
||||
/>
|
||||
<InputGroup
|
||||
{...register("submitButtonText")}
|
||||
label="Submit Button Text"
|
||||
name="submitButtonText"
|
||||
type="text"
|
||||
placeholder="Enter Submit Button Text"
|
||||
/>
|
||||
</div>
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection title="Form Fields" className="flex flex-col gap-5">
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="grid grid-cols-1 items-center gap-4 sm:grid-cols-2"
|
||||
>
|
||||
<InputGroup
|
||||
{...register(`fields.${index}.name`)}
|
||||
label="Field Name"
|
||||
name={`fields.${index}.name`}
|
||||
type="text"
|
||||
placeholder="Field Name"
|
||||
disabled
|
||||
/>
|
||||
<InputGroup
|
||||
{...register(`fields.${index}.label`)}
|
||||
label="Label"
|
||||
name={`fields.${index}.label`}
|
||||
type="text"
|
||||
placeholder="Enter Label"
|
||||
/>
|
||||
<InputGroup
|
||||
{...register(`fields.${index}.placeholder`)}
|
||||
label="Placeholder"
|
||||
name={`fields.${index}.placeholder`}
|
||||
type="text"
|
||||
placeholder="Enter Placeholder"
|
||||
/>
|
||||
<Checkbox
|
||||
{...register(`fields.${index}.required`)}
|
||||
label="Required"
|
||||
minimal
|
||||
withIcon="check"
|
||||
withBg
|
||||
name={`fields.${index}.required`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ShowcaseSection>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContactsStep.displayName = "ContactsStep";
|
||||
|
||||
export default ContactsStep;
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import SectionStep from "./SectionStep";
|
||||
import useFeaturesStepPresenter, {
|
||||
IFeaturesStepFields,
|
||||
} from "./useFeaturesStepPresenter";
|
||||
|
||||
interface FeaturesStepProps {
|
||||
initialData?: IFeaturesStepFields;
|
||||
}
|
||||
|
||||
const FeaturesStep = forwardRef<unknown, FeaturesStepProps>(
|
||||
({ initialData }, ref) => {
|
||||
const {
|
||||
errors,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
register,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
getValues,
|
||||
} = useFeaturesStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<SectionStep<IFeaturesStepFields>
|
||||
title="Features"
|
||||
fields={fields}
|
||||
handleSubmit={handleSubmit}
|
||||
onSubmit={onSubmit}
|
||||
register={register}
|
||||
errors={errors}
|
||||
append={append}
|
||||
remove={remove}
|
||||
control={control}
|
||||
fieldName="cards"
|
||||
defaultValues={{ icon: "", title: "", description: "" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FeaturesStep.displayName = "FeaturesStep";
|
||||
|
||||
export default FeaturesStep;
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import useFooterStepPresenter from "./useFooterStepPresenter";
|
||||
|
||||
interface FooterStepProps {
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const FooterStep = forwardRef<unknown, FooterStepProps>(
|
||||
({ initialData }, ref) => {
|
||||
const { errors, handleSubmit, onSubmit, register, getValues } =
|
||||
useFooterStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<ShowcaseSection title="Footer Section" className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-9">
|
||||
<InputGroup
|
||||
{...register("email", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
pattern: {
|
||||
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: "Invalid email format",
|
||||
},
|
||||
})}
|
||||
label="Email"
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
placeholder="Enter Email"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.email.message as string}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("phone")}
|
||||
label="Phone Number"
|
||||
name="phone"
|
||||
type="text"
|
||||
placeholder="Enter Phone Number"
|
||||
/>
|
||||
<InputGroup
|
||||
{...register("location")}
|
||||
label="Location"
|
||||
name="location"
|
||||
type="text"
|
||||
placeholder="Enter Location"
|
||||
/>
|
||||
<TextAreaGroup
|
||||
label="Tagline"
|
||||
{...register("tagline")}
|
||||
name="tagline"
|
||||
placeholder="Enter Tagline"
|
||||
/>
|
||||
<InputGroup
|
||||
{...register("copyright")}
|
||||
label="Copyright"
|
||||
name="copyright"
|
||||
type="text"
|
||||
placeholder="Enter Copyright Text"
|
||||
/>
|
||||
</div>
|
||||
</ShowcaseSection>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FooterStep.displayName = "FooterStep";
|
||||
|
||||
export default FooterStep;
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import { REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import useHeroStepPresenter from "./useHeroStepPresenter";
|
||||
|
||||
interface HeroStepProps {
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const HeroStep = forwardRef<unknown, HeroStepProps>(({ initialData }, ref) => {
|
||||
const { errors, handleSubmit, onSubmit, register, getValues } =
|
||||
useHeroStepPresenter(initialData);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => getValues(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<form
|
||||
className="items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7 sm:grid-cols-2"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
title="Hero Information"
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<InputGroup
|
||||
{...register("title", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 200,
|
||||
message: FormErrorMessages.maxLength(200),
|
||||
},
|
||||
})}
|
||||
label="Title"
|
||||
name="title"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.title.message}</p>
|
||||
)}
|
||||
<TextAreaGroup
|
||||
label="Description"
|
||||
{...register("description", {
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: FormErrorMessages.minLength(10),
|
||||
},
|
||||
maxLength: {
|
||||
value: 1000,
|
||||
message: FormErrorMessages.maxLength(1000),
|
||||
},
|
||||
})}
|
||||
name="description"
|
||||
placeholder="Enter Description"
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.description.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("buttonText", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: FormErrorMessages.maxLength(50),
|
||||
},
|
||||
})}
|
||||
label="Button Text"
|
||||
name="buttonText"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Button Text"
|
||||
/>
|
||||
{errors.buttonText && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.buttonText.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("buttonLink", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: FormErrorMessages.minLength(2),
|
||||
},
|
||||
maxLength: {
|
||||
value: 200,
|
||||
message: FormErrorMessages.maxLength(200),
|
||||
},
|
||||
})}
|
||||
label="Button Link"
|
||||
name="buttonLink"
|
||||
required
|
||||
type="url"
|
||||
placeholder="Enter Button Link"
|
||||
/>
|
||||
{errors.buttonLink && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.buttonLink.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...register("gifFile", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
pattern: {
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
value: REGEX.URL,
|
||||
},
|
||||
})}
|
||||
label="GIF File URL"
|
||||
name="gifFile"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter GIF File URL"
|
||||
/>
|
||||
{errors.gifFile && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.gifFile.message}
|
||||
</p>
|
||||
)}
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
HeroStep.displayName = "HeroStep";
|
||||
|
||||
export default HeroStep;
|
||||
@@ -0,0 +1,371 @@
|
||||
"use client";
|
||||
import { GlobeIcon } from "@/assets/icons";
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import Stepper, { Step } from "@/components/ui/Stepper";
|
||||
import { Countries } from "@/constants/countries";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import BenefitsStep from "./BenefitsStep";
|
||||
import ChallengesStep from "./ChallengesStep";
|
||||
import ChartStep from "./ChartStep";
|
||||
import ContactsStep from "./ContactsStep";
|
||||
import FeaturesStep from "./FeaturesStep";
|
||||
import FooterStep from "./FooterStep";
|
||||
import HeroStep from "./HeroStep";
|
||||
|
||||
interface IPageTypeFields {
|
||||
type: "company" | "organization";
|
||||
language?: string;
|
||||
company_name?: string;
|
||||
country?: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface MarketingFormProps {
|
||||
breadcrumbItems: BreadcrumbItem[];
|
||||
title: string;
|
||||
onSubmit: (data: any) => void;
|
||||
isPending: boolean;
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
const MarketingForm = ({
|
||||
breadcrumbItems,
|
||||
title,
|
||||
onSubmit,
|
||||
isPending,
|
||||
initialData,
|
||||
}: MarketingFormProps) => {
|
||||
const [step, setStep] = useState(0);
|
||||
const [formData, setFormData] = useState<any>(initialData || {});
|
||||
const heroRef = useRef<any>(null);
|
||||
const chartRef = useRef<any>(null);
|
||||
const featuresRef = useRef<any>(null);
|
||||
const challengesRef = useRef<any>(null);
|
||||
const benefitsRef = useRef<any>(null);
|
||||
const contactsRef = useRef<any>(null);
|
||||
const footerRef = useRef<any>(null);
|
||||
|
||||
const {
|
||||
register: registerPageType,
|
||||
watch: watchPageType,
|
||||
formState: { errors: pageTypeErrors },
|
||||
getValues: getPageTypeValues,
|
||||
setValue,
|
||||
reset,
|
||||
} = useForm<IPageTypeFields>({
|
||||
defaultValues: {
|
||||
type: "company",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
const formValues = {
|
||||
type: initialData.type || "company",
|
||||
language: initialData.language || "",
|
||||
company_name: initialData.company_name || "",
|
||||
country: initialData.country || "",
|
||||
key: initialData.key || "",
|
||||
};
|
||||
reset(formValues);
|
||||
setFormData(initialData);
|
||||
}
|
||||
}, [initialData, reset]);
|
||||
|
||||
const pageType = watchPageType("type");
|
||||
const isCompany = pageType === "company";
|
||||
|
||||
const saveCurrentStepData = async () => {
|
||||
const refs = [
|
||||
heroRef,
|
||||
chartRef,
|
||||
featuresRef,
|
||||
challengesRef,
|
||||
benefitsRef,
|
||||
contactsRef,
|
||||
footerRef,
|
||||
];
|
||||
|
||||
const keys = [
|
||||
"hero",
|
||||
"chart",
|
||||
"features",
|
||||
"challenges",
|
||||
"advantages",
|
||||
"contact",
|
||||
"footer",
|
||||
];
|
||||
|
||||
const currentRef = refs[step];
|
||||
if (currentRef?.current?.getData) {
|
||||
try {
|
||||
const data = await currentRef.current.getData();
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
[keys[step]]: data,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`Error collecting data from step ${step}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collectAllStepsData = async () => {
|
||||
await saveCurrentStepData();
|
||||
|
||||
const refs = [
|
||||
heroRef,
|
||||
chartRef,
|
||||
featuresRef,
|
||||
challengesRef,
|
||||
benefitsRef,
|
||||
contactsRef,
|
||||
footerRef,
|
||||
];
|
||||
|
||||
const allData: any = { ...formData };
|
||||
|
||||
const keys = [
|
||||
"hero",
|
||||
"chart",
|
||||
"features",
|
||||
"challenges",
|
||||
"advantages",
|
||||
"contact",
|
||||
"footer",
|
||||
];
|
||||
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
const ref = refs[i];
|
||||
if (ref?.current?.getData) {
|
||||
try {
|
||||
const data = await ref.current.getData();
|
||||
allData[keys[i]] = data;
|
||||
} catch (error) {
|
||||
console.error(`Error collecting data from step ${i}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allData;
|
||||
};
|
||||
|
||||
const handleSubmitAll = async () => {
|
||||
const allData = await collectAllStepsData();
|
||||
const pageTypeData = getPageTypeValues();
|
||||
|
||||
const finalData = {
|
||||
...allData,
|
||||
type: pageTypeData.type,
|
||||
key: pageTypeData.key,
|
||||
...(pageTypeData.type === "company" && {
|
||||
language: pageTypeData.language,
|
||||
company_name: pageTypeData.company_name,
|
||||
country: pageTypeData.country,
|
||||
}),
|
||||
};
|
||||
|
||||
onSubmit(finalData);
|
||||
};
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
title: "Hero",
|
||||
description: "Create the hero section",
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
title: "Chart",
|
||||
description: "Create the chart",
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
title: "Features",
|
||||
description: "Add features",
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
title: "Challenges",
|
||||
description: "Create challenges section",
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
title: "Benefits",
|
||||
description: "create benefits section",
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
title: "Contacts",
|
||||
description: "Create contacts section",
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
title: "Footer",
|
||||
description: "Add footer information",
|
||||
content: <></>,
|
||||
},
|
||||
];
|
||||
|
||||
const handleStepChange = async (newStep: number) => {
|
||||
await saveCurrentStepData();
|
||||
setStep(newStep);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<ShowcaseSection title={title}>
|
||||
<div className="mb-6 rounded-xl bg-gray-1 p-6 dark:bg-gray-7">
|
||||
<h3 className="mb-4 text-lg font-semibold text-dark dark:text-white">
|
||||
Page Configuration
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<Select
|
||||
{...registerPageType("type", {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
name="type"
|
||||
label="Page Type"
|
||||
required
|
||||
items={[
|
||||
{ value: "company", label: "Company" },
|
||||
{ value: "organization", label: "Organization" },
|
||||
]}
|
||||
/>
|
||||
{pageTypeErrors.type && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{pageTypeErrors.type.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<InputGroup
|
||||
{...registerPageType("language", {
|
||||
required: isCompany
|
||||
? { message: FormErrorMessages.required, value: true }
|
||||
: false,
|
||||
minLength: isCompany
|
||||
? { value: 2, message: FormErrorMessages.minLength(2) }
|
||||
: undefined,
|
||||
maxLength: isCompany
|
||||
? { value: 10, message: FormErrorMessages.maxLength(10) }
|
||||
: undefined,
|
||||
})}
|
||||
label="Language"
|
||||
name="language"
|
||||
type="text"
|
||||
placeholder="Enter Language (e.g., en, ar)"
|
||||
disabled={!isCompany}
|
||||
required={isCompany}
|
||||
/>
|
||||
{pageTypeErrors.language && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{pageTypeErrors.language.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<InputGroup
|
||||
{...registerPageType("company_name", {
|
||||
required: isCompany
|
||||
? { message: FormErrorMessages.required, value: true }
|
||||
: false,
|
||||
minLength: isCompany
|
||||
? { value: 2, message: FormErrorMessages.minLength(2) }
|
||||
: undefined,
|
||||
maxLength: isCompany
|
||||
? { value: 200, message: FormErrorMessages.maxLength(200) }
|
||||
: undefined,
|
||||
})}
|
||||
label="Company Name"
|
||||
name="company_name"
|
||||
type="text"
|
||||
placeholder="Enter Company Name"
|
||||
disabled={!isCompany}
|
||||
required={isCompany}
|
||||
/>
|
||||
{pageTypeErrors.company_name && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{pageTypeErrors.company_name.message}
|
||||
</p>
|
||||
)}
|
||||
<Select
|
||||
{...registerPageType("country")}
|
||||
name="country"
|
||||
label="Country"
|
||||
items={Countries}
|
||||
defaultValue="USA"
|
||||
clearable
|
||||
prefixIcon={<GlobeIcon />}
|
||||
disabled={!isCompany}
|
||||
required={isCompany}
|
||||
placeholder="Please select country"
|
||||
onClear={() => setValue("country", undefined)}
|
||||
/>
|
||||
{pageTypeErrors.country && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{pageTypeErrors.country.message}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup
|
||||
{...registerPageType("key", {
|
||||
required: {
|
||||
value: true,
|
||||
message: FormErrorMessages.required,
|
||||
},
|
||||
})}
|
||||
label="Page key"
|
||||
name="key"
|
||||
placeholder="Please enter page key"
|
||||
type="text"
|
||||
/>
|
||||
{pageTypeErrors.key && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{pageTypeErrors.key.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: step === 0 ? "block" : "none" }}>
|
||||
<HeroStep ref={heroRef} initialData={formData.hero} />
|
||||
</div>
|
||||
<div style={{ display: step === 1 ? "block" : "none" }}>
|
||||
<ChartStep ref={chartRef} initialData={formData.chart} />
|
||||
</div>
|
||||
<div style={{ display: step === 2 ? "block" : "none" }}>
|
||||
<FeaturesStep ref={featuresRef} initialData={formData.features} />
|
||||
</div>
|
||||
<div style={{ display: step === 3 ? "block" : "none" }}>
|
||||
<ChallengesStep
|
||||
ref={challengesRef}
|
||||
initialData={formData.challenges}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: step === 4 ? "block" : "none" }}>
|
||||
<BenefitsStep ref={benefitsRef} initialData={formData.advantages} />
|
||||
</div>
|
||||
<div style={{ display: step === 5 ? "block" : "none" }}>
|
||||
<ContactsStep ref={contactsRef} initialData={formData.contact} />
|
||||
</div>
|
||||
<div style={{ display: step === 6 ? "block" : "none" }}>
|
||||
<FooterStep ref={footerRef} initialData={formData.footer} />
|
||||
</div>
|
||||
|
||||
<Stepper
|
||||
steps={steps}
|
||||
currentStep={step}
|
||||
onStepChange={handleStepChange}
|
||||
onSubmit={handleSubmitAll}
|
||||
isSubmitting={isPending}
|
||||
/>
|
||||
</ShowcaseSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingForm;
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
import { TrashIcon } from "@/assets/icons";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import { REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import {
|
||||
Control,
|
||||
FieldErrors,
|
||||
FieldValues,
|
||||
Path,
|
||||
UseFieldArrayAppend,
|
||||
UseFieldArrayRemove,
|
||||
UseFormHandleSubmit,
|
||||
UseFormRegister,
|
||||
} from "react-hook-form";
|
||||
|
||||
interface IProps<T extends FieldValues> {
|
||||
title: string;
|
||||
fields: any[];
|
||||
handleSubmit: UseFormHandleSubmit<T>;
|
||||
onSubmit: (data: T) => void;
|
||||
register: UseFormRegister<T>;
|
||||
errors: FieldErrors<T>;
|
||||
append: UseFieldArrayAppend<T, any>;
|
||||
remove: UseFieldArrayRemove;
|
||||
control: Control<T, any>;
|
||||
fieldName: string;
|
||||
defaultValues: any;
|
||||
}
|
||||
|
||||
const SectionStep = <T extends FieldValues>({
|
||||
title,
|
||||
fields,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
register,
|
||||
errors,
|
||||
append,
|
||||
remove,
|
||||
fieldName,
|
||||
defaultValues,
|
||||
}: IProps<T>) => {
|
||||
return (
|
||||
<form
|
||||
className="grid grid-cols-1 items-start gap-9 rounded-xl bg-gray-1 p-10 dark:bg-gray-7"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-col gap-9">
|
||||
<ShowcaseSection
|
||||
title={`${title} Section`}
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
<InputGroup
|
||||
{...register("title" as Path<T>, {
|
||||
required: { message: FormErrorMessages.required, value: true },
|
||||
})}
|
||||
label="Section Title"
|
||||
name="title"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Section Title"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
{errors.title.message as string}
|
||||
</p>
|
||||
)}
|
||||
<TextAreaGroup
|
||||
label="Section Description"
|
||||
{...register("description" as Path<T>)}
|
||||
name="description"
|
||||
placeholder="Enter Section Description"
|
||||
/>
|
||||
</ShowcaseSection>
|
||||
<ShowcaseSection
|
||||
title={`${title} cards`}
|
||||
className="flex flex-col gap-5"
|
||||
>
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-2"
|
||||
>
|
||||
<InputGroup
|
||||
{...register(`cards.${index}.icon` as Path<T>, {
|
||||
required: {
|
||||
message: FormErrorMessages.required,
|
||||
value: true,
|
||||
},
|
||||
pattern: {
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
value: REGEX.URL,
|
||||
},
|
||||
})}
|
||||
label="Icon"
|
||||
name={`cards.${index}.icon`}
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Icon URL"
|
||||
/>
|
||||
<InputGroup
|
||||
{...register(`cards.${index}.title` as Path<T>, {
|
||||
required: {
|
||||
message: FormErrorMessages.required,
|
||||
value: true,
|
||||
},
|
||||
})}
|
||||
label="Title"
|
||||
name={`cards.${index}.title`}
|
||||
required
|
||||
type="text"
|
||||
placeholder="Enter Title"
|
||||
/>
|
||||
<TextAreaGroup
|
||||
{...register(`cards.${index}.description` as Path<T>)}
|
||||
label="Description"
|
||||
name={`cards.${index}.description`}
|
||||
placeholder="Enter Description"
|
||||
className="sm:col-span-2"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 flex w-fit items-center justify-center rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||
onClick={() => append(defaultValues)}
|
||||
>
|
||||
Add point
|
||||
</button>
|
||||
</ShowcaseSection>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionStep;
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
export interface IBenefitsStepFields {
|
||||
title: string;
|
||||
description: string;
|
||||
cards: {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const useBenefitsStepPresenter = (initialData?: IBenefitsStepFields) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<IBenefitsStepFields>({
|
||||
defaultValues: initialData || {
|
||||
cards: [{ icon: "", title: "", description: "" }],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "cards",
|
||||
});
|
||||
|
||||
const onSubmit = (data: IBenefitsStepFields) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const collectData = async () => {
|
||||
return getValues();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
onSubmit,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
watch,
|
||||
getValues,
|
||||
collectData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useBenefitsStepPresenter;
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
export interface IChallengesStepFields {
|
||||
title: string;
|
||||
description: string;
|
||||
cards: {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const useChallengesStepPresenter = (initialData?: IChallengesStepFields) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<IChallengesStepFields>({
|
||||
defaultValues: initialData || {
|
||||
cards: [{ icon: "", title: "", description: "" }],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "cards",
|
||||
});
|
||||
|
||||
const onSubmit = (data: IChallengesStepFields) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const collectData = async () => {
|
||||
return getValues();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
onSubmit,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
watch,
|
||||
getValues,
|
||||
collectData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useChallengesStepPresenter;
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
export interface IChartStepFields {
|
||||
title: string;
|
||||
missedAmount: string;
|
||||
data: { key: string; value: number }[];
|
||||
}
|
||||
|
||||
const useChartStepPresenter = (initialData?: IChartStepFields) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<IChartStepFields>({
|
||||
defaultValues: initialData || {
|
||||
data: [{ key: "", value: 0 }],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "data",
|
||||
});
|
||||
|
||||
const onSubmit = (data: IChartStepFields) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const collectData = async () => {
|
||||
return getValues();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
onSubmit,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
watch,
|
||||
getValues,
|
||||
collectData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useChartStepPresenter;
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
export interface IContactsStepFields {
|
||||
title: string;
|
||||
description: string;
|
||||
submitButtonText: string;
|
||||
fields: {
|
||||
name: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
required: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
const useContactsStepPresenter = (initialData?: IContactsStepFields) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useForm<IContactsStepFields>({
|
||||
defaultValues: initialData || {
|
||||
fields: [
|
||||
{
|
||||
name: "first_name",
|
||||
label: "",
|
||||
placeholder: "",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "last_name",
|
||||
label: "",
|
||||
placeholder: "",
|
||||
required: false,
|
||||
},
|
||||
{ name: "email", label: "", placeholder: "", required: false },
|
||||
{ name: "phone", label: "", placeholder: "", required: false },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: "fields",
|
||||
});
|
||||
|
||||
const onSubmit = (data: IContactsStepFields) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const collectData = async () => {
|
||||
return getValues();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
onSubmit,
|
||||
fields,
|
||||
control,
|
||||
collectData,
|
||||
getValues,
|
||||
};
|
||||
};
|
||||
|
||||
export default useContactsStepPresenter;
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
export interface IFeaturesStepFields {
|
||||
title: string;
|
||||
description: string;
|
||||
cards: {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const useFeaturesStepPresenter = (initialData?: IFeaturesStepFields) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<IFeaturesStepFields>({
|
||||
defaultValues: initialData || {
|
||||
cards: [{ icon: "", title: "", description: "" }],
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "cards",
|
||||
});
|
||||
|
||||
const onSubmit = (data: IFeaturesStepFields) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const collectData = async () => {
|
||||
return getValues();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
onSubmit,
|
||||
fields,
|
||||
append,
|
||||
remove,
|
||||
control,
|
||||
watch,
|
||||
getValues,
|
||||
collectData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFeaturesStepPresenter;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
interface FooterFormValues {
|
||||
email: string;
|
||||
phone: string;
|
||||
location: string;
|
||||
tagline: string;
|
||||
copyright: string;
|
||||
}
|
||||
|
||||
const useFooterStepPresenter = (initialData?: FooterFormValues) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<FooterFormValues>({
|
||||
defaultValues: initialData,
|
||||
});
|
||||
|
||||
const onSubmit = (data: FooterFormValues) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
const collectData = async () => {
|
||||
return getValues();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
errors,
|
||||
collectData,
|
||||
getValues,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFooterStepPresenter;
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export interface IHeroStepFields {
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
gifFile: string;
|
||||
}
|
||||
|
||||
const useHeroStepPresenter = (initialData?: IHeroStepFields) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
} = useForm<IHeroStepFields>({
|
||||
defaultValues: initialData,
|
||||
});
|
||||
|
||||
const onSubmit = (data: IHeroStepFields) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit,
|
||||
errors,
|
||||
onSubmit,
|
||||
getValues,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHeroStepPresenter;
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import { useCreateCms } from "@/hooks/queries/useCmsQueries";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import MarketingForm from "../_components/MarketingForm";
|
||||
|
||||
const Marketing = () => {
|
||||
const { mutate: createCms, isPending } = useCreateCms();
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "/marketing",
|
||||
name: "Marketing",
|
||||
},
|
||||
{
|
||||
href: "/marketing/create",
|
||||
name: "Create Marketing",
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
createCms(data, {
|
||||
onSuccess: () => {
|
||||
console.log("CMS created successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error creating CMS:", error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MarketingForm
|
||||
breadcrumbItems={breadcrumbItems}
|
||||
title="Create Marketing page"
|
||||
onSubmit={handleSubmit}
|
||||
isPending={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Marketing;
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import Loading from "@/app/loading";
|
||||
import { useGetCmsDetails, useUpdateCms } from "@/hooks/queries/useCmsQueries";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import { use } from "react";
|
||||
import MarketingForm from "../../_components/MarketingForm";
|
||||
|
||||
interface IProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
const EditMarketingPage = ({ params }: IProps) => {
|
||||
const { id } = use(params);
|
||||
const { data, isPending: isLoading } = useGetCmsDetails(id);
|
||||
const { mutate: updateCms, isPending: isUpdating } = useUpdateCms(id);
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "/marketing",
|
||||
name: "Marketing",
|
||||
},
|
||||
{
|
||||
href: `/marketing/${id}`,
|
||||
name: "Edit Marketing",
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = (formData: any) => {
|
||||
updateCms(formData, {
|
||||
onSuccess: () => {
|
||||
console.log("CMS updated successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error updating CMS:", error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading || !data?.data) return <Loading />;
|
||||
|
||||
return (
|
||||
<MarketingForm
|
||||
breadcrumbItems={breadcrumbItems}
|
||||
title="Edit Marketing page"
|
||||
onSubmit={handleSubmit}
|
||||
isPending={isUpdating}
|
||||
initialData={data.data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditMarketingPage;
|
||||
@@ -0,0 +1,24 @@
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import CmsTable from "@/components/Tables/cms";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
|
||||
const MarketingPage = () => {
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{
|
||||
href: "/",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "/marketing",
|
||||
name: "Marketing",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<CmsTable />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingPage;
|
||||
@@ -38,6 +38,36 @@ export function CurrencyIcon(props: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function FilterIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5.40002 2.09998H18.6C19.7 2.09998 20.6 2.99998 20.6 4.09998V6.29998C20.6 7.09998 20.1 8.09998 19.6 8.59998L15.3 12.4C14.7 12.9 14.3 13.9 14.3 14.7V19C14.3 19.6 13.9 20.4 13.4 20.7L12 21.6C10.7 22.4 8.90002 21.5 8.90002 19.9V14.6C8.90002 13.9 8.50002 13 8.10002 12.5L4.30002 8.49998C3.80002 7.99998 3.40002 7.09998 3.40002 6.49998V4.19998C3.40002 2.99998 4.30002 2.09998 5.40002 2.09998Z"
|
||||
stroke="#9299A6"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.93 2.09998L6 9.99998"
|
||||
stroke="#9299A6"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoneyIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
@@ -153,6 +183,67 @@ export function TrendingUpIcon(props: IconProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function GlobeSearch(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22 12C22 6.48 17.52 2 12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.99998 3H8.99998C7.04998 8.84 7.04998 15.16 8.99998 21H7.99998"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15 3C15.97 5.92 16.46 8.96 16.46 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 16V15C5.92 15.97 8.96 16.46 12 16.46"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 9.0001C8.84 7.0501 15.16 7.0501 21 9.0001"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M18.2 21.4C19.9673 21.4 21.4 19.9673 21.4 18.2C21.4 16.4327 19.9673 15 18.2 15C16.4327 15 15 16.4327 15 18.2C15 19.9673 16.4327 21.4 18.2 21.4Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M22 22L21 21"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function CheckIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import GoogleSigninButton from "../GoogleSigninButton";
|
||||
import SigninWithPassword from "../SigninWithPassword";
|
||||
|
||||
export default function Signin() {
|
||||
|
||||
@@ -43,7 +43,7 @@ export function Checkbox({
|
||||
|
||||
<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",
|
||||
"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 transition-all duration-300",
|
||||
withBg
|
||||
? "peer-checked:bg-primary [&>*]:text-white"
|
||||
: "peer-checked:bg-gray-2 dark:peer-checked:bg-transparent",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ChevronUpIcon } from "@/assets/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ILabelValue } from "@/types/shared";
|
||||
import { useId, useState, type ChangeEvent } from "react";
|
||||
import { useEffect, useId, useState, type ChangeEvent } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
@@ -22,8 +22,11 @@ type PropsType<T extends FieldValues> = {
|
||||
register?: UseFormRegister<T>;
|
||||
errors?: FieldErrors<T>;
|
||||
required?: boolean;
|
||||
clearable?: boolean;
|
||||
onClear?: () => void;
|
||||
value?: string;
|
||||
} & (
|
||||
| { placeholder?: string; defaultValue: string }
|
||||
| { placeholder?: string; defaultValue?: string }
|
||||
| { placeholder: string; defaultValue?: string }
|
||||
) &
|
||||
Partial<UseFormRegisterReturn>;
|
||||
@@ -42,13 +45,38 @@ export function Select<T extends FieldValues>({
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
clearable = false,
|
||||
onClear,
|
||||
value: controlledValue,
|
||||
...props
|
||||
}: PropsType<T>) {
|
||||
const id = useId();
|
||||
|
||||
const [isOptionSelected, setIsOptionSelected] = useState(!!defaultValue);
|
||||
const [value, setValue] = useState(defaultValue || "");
|
||||
const error = errors && errors[name];
|
||||
|
||||
useEffect(() => {
|
||||
if (controlledValue !== undefined) {
|
||||
setValue(controlledValue);
|
||||
setIsOptionSelected(!!controlledValue);
|
||||
}
|
||||
}, [controlledValue]);
|
||||
|
||||
const handleClear = () => {
|
||||
if (controlledValue === undefined) {
|
||||
setValue("");
|
||||
}
|
||||
setIsOptionSelected(false);
|
||||
onClear?.();
|
||||
if (onChange) {
|
||||
const event = {
|
||||
target: { value: "", name },
|
||||
} as unknown as ChangeEvent<HTMLSelectElement>;
|
||||
onChange(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3", className)}>
|
||||
<label
|
||||
@@ -70,8 +98,11 @@ export function Select<T extends FieldValues>({
|
||||
id={id}
|
||||
name={name}
|
||||
ref={ref}
|
||||
defaultValue={defaultValue || ""}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (controlledValue === undefined) {
|
||||
setValue(e.target.value);
|
||||
}
|
||||
setIsOptionSelected(!!e.target.value);
|
||||
onChange?.(e);
|
||||
}}
|
||||
@@ -81,13 +112,10 @@ export function Select<T extends FieldValues>({
|
||||
isOptionSelected && "text-dark dark:text-white",
|
||||
error && "border-red focus:border-red dark:border-red",
|
||||
prefixIcon && "pl-11.5",
|
||||
clearable && value && "pr-20",
|
||||
)}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled hidden>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{placeholder && <option value="">{placeholder}</option>}
|
||||
|
||||
{items.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
@@ -96,6 +124,30 @@ export function Select<T extends FieldValues>({
|
||||
))}
|
||||
</select>
|
||||
|
||||
{clearable && value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="absolute right-10 top-1/2 -translate-y-1/2 text-dark-5 hover:text-dark dark:text-dark-6 dark:hover:text-white"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 4L4 12M4 4L12 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ChevronUpIcon className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 rotate-180" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface EmptyListWrapperProps<T> {
|
||||
list: T[];
|
||||
emptyMessage: string;
|
||||
columns: string[];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function EmptyListWrapper<T>({
|
||||
list,
|
||||
emptyMessage,
|
||||
columns,
|
||||
children,
|
||||
}: EmptyListWrapperProps<T>) {
|
||||
if (!list.length) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="text-center" colSpan={columns.length * 100}>
|
||||
<h2>{emptyMessage}</h2>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default EmptyListWrapper;
|
||||
@@ -107,6 +107,7 @@ export function UserInfo() {
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
logout();
|
||||
|
||||
}}
|
||||
>
|
||||
<LogOutIcon />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GlobeSearch } from "@/assets/icons";
|
||||
import { z } from "zod";
|
||||
import * as Icons from "../icons";
|
||||
|
||||
@@ -72,6 +73,19 @@ export const NAV_DATA: NavData = [
|
||||
icon: Icons.NotificationIcon,
|
||||
items: [],
|
||||
},
|
||||
// {
|
||||
// title: "Contact Us",
|
||||
// url: "/contact-us",
|
||||
// icon: Icons.CallIncome,
|
||||
// items: [],
|
||||
// visible: false,
|
||||
// },
|
||||
{
|
||||
title: "Marketing",
|
||||
url: "/marketing",
|
||||
icon: GlobeSearch,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -19,6 +19,32 @@ export function ChevronUp(props: PropsType) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function CallIncome(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M21.97 18.33C21.97 18.69 21.89 19.06 21.72 19.42C21.55 19.78 21.33 20.12 21.04 20.44C20.55 20.98 20.01 21.37 19.4 21.62C18.8 21.87 18.15 22 17.45 22C16.43 22 15.34 21.76 14.19 21.27C13.04 20.78 11.89 20.12 10.75 19.29C9.6 18.45 8.51 17.52 7.47 16.49C6.44 15.45 5.51 14.36 4.68 13.22C3.86 12.08 3.2 10.94 2.72 9.81C2.24 8.67 2 7.58 2 6.54C2 5.86 2.12 5.21 2.36 4.61C2.6 4 2.98 3.44 3.51 2.94C4.15 2.31 4.85 2 5.59 2C5.87 2 6.15 2.06 6.4 2.18C6.66 2.3 6.89 2.48 7.07 2.74L9.39 6.01C9.57 6.26 9.7 6.49 9.79 6.71C9.88 6.92 9.93 7.13 9.93 7.32C9.93 7.56 9.86 7.8 9.72 8.03C9.59 8.26 9.4 8.5 9.16 8.74L8.4 9.53C8.29 9.64 8.24 9.77 8.24 9.93C8.24 10.01 8.25 10.08 8.27 10.16C8.3 10.24 8.33 10.3 8.35 10.36C8.53 10.69 8.84 11.12 9.28 11.64C9.73 12.16 10.21 12.69 10.73 13.22C11.27 13.75 11.79 14.24 12.32 14.69C12.84 15.13 13.27 15.43 13.61 15.61C13.66 15.63 13.72 15.66 13.79 15.69C13.87 15.72 13.95 15.73 14.04 15.73C14.21 15.73 14.34 15.67 14.45 15.56L15.21 14.81C15.46 14.56 15.7 14.37 15.93 14.25C16.16 14.11 16.39 14.04 16.64 14.04C16.83 14.04 17.03 14.08 17.25 14.17C17.47 14.26 17.7 14.39 17.95 14.56L21.26 16.91C21.52 17.09 21.7 17.3 21.81 17.55C21.91 17.8 21.97 18.05 21.97 18.33Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M16.2 7.8H21M16.2 7.8V3V7.8Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function NotificationIcon(props: PropsType) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { AdminStatus } from "@/constants/enums";
|
||||
import { getEnumAsArray } from "@/utils/shared";
|
||||
import {
|
||||
FieldValues,
|
||||
UseFormHandleSubmit,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
UseFormWatch,
|
||||
} from "react-hook-form";
|
||||
|
||||
interface AdminListFiltersProps {
|
||||
filterRegister: UseFormRegister<FieldValues>;
|
||||
setIsFilterModalOpen: (isOpen: boolean) => void;
|
||||
isMutating: number;
|
||||
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
|
||||
search: (data: any) => void;
|
||||
watch: UseFormWatch<FieldValues>;
|
||||
setValue: UseFormSetValue<any>;
|
||||
}
|
||||
|
||||
const AdminListFilters = ({
|
||||
filterRegister,
|
||||
setIsFilterModalOpen,
|
||||
isMutating,
|
||||
handleFilterSubmit,
|
||||
search,
|
||||
watch,
|
||||
setValue,
|
||||
}: AdminListFiltersProps) => {
|
||||
return (
|
||||
<form
|
||||
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
|
||||
onSubmit={handleFilterSubmit(search)}
|
||||
>
|
||||
<Select
|
||||
{...filterRegister("status")}
|
||||
items={getEnumAsArray(AdminStatus)}
|
||||
label="status"
|
||||
name="status"
|
||||
placeholder="Status"
|
||||
clearable
|
||||
value={watch("status")}
|
||||
onClear={() => setValue("status", undefined)}
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("search")}
|
||||
name="search"
|
||||
label="search"
|
||||
placeholder="search"
|
||||
type="search"
|
||||
/>
|
||||
{/* <InputGroup
|
||||
{...filterRegister("email")}
|
||||
name="email"
|
||||
label="email"
|
||||
placeholder="email"
|
||||
type="email"
|
||||
/> */}
|
||||
{/* <Select
|
||||
{...filterRegister("role")}
|
||||
name="role"
|
||||
items={getEnumAsArray(AdminRoles)}
|
||||
label="Role"
|
||||
placeholder="Role"
|
||||
clearable
|
||||
value={watch("role")}
|
||||
onClear={() => setValue("role", undefined)}
|
||||
/> */}
|
||||
{/* <InputGroup
|
||||
{...filterRegister("username")}
|
||||
label="username"
|
||||
name="username"
|
||||
placeholder="username"
|
||||
type="text"
|
||||
/> */}
|
||||
{/* <InputGroup
|
||||
{...filterRegister("full_name")}
|
||||
label="full name"
|
||||
name="full_name"
|
||||
placeholder="full name"
|
||||
type="text"
|
||||
/> */}
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||
>
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
<span>Search</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||
onClick={() => setIsFilterModalOpen(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminListFilters;
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import Status from "@/components/ui/Status";
|
||||
import {
|
||||
Table,
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { AdminStatus } from "@/constants/enums";
|
||||
import Link from "next/link";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import AdminListFilters from "./AdminListFilters";
|
||||
import ChangeStatusModalContent from "./ChangeStatusModalContent";
|
||||
import { useAdminsPresenter } from "./useAdminsPresenter";
|
||||
|
||||
@@ -32,18 +33,26 @@ const AdminsTable = () => {
|
||||
setIsChangeStatusModalOpen,
|
||||
columns,
|
||||
setCurrentUser,
|
||||
pathName,
|
||||
isFilterModalOpen,
|
||||
setIsFilterModalOpen,
|
||||
changeStatus,
|
||||
currentUser,
|
||||
setStatus,
|
||||
status,
|
||||
setFilterValue,
|
||||
handleFilterSubmit,
|
||||
search,
|
||||
filterRegister,
|
||||
isMutating,
|
||||
watch,
|
||||
} = useAdminsPresenter();
|
||||
|
||||
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>
|
||||
<ListHeader
|
||||
onFilter={() => setIsFilterModalOpen(true)}
|
||||
createButtonText="Create Admin"
|
||||
/>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||
@@ -56,79 +65,85 @@ const AdminsTable = () => {
|
||||
</TableHeader>
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{allUsers.map((item, index) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<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" colSpan={100}>
|
||||
<Status status={item.status ?? ""}>{item.status}</Status>
|
||||
</TableCell>
|
||||
{!allUsers.length ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<h2>No user</h2>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
allUsers.map((item, index) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={item?.id || index}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item?.full_name}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item?.email}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
data-tooltip-id="delete"
|
||||
data-tooltip-content="Delete"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
|
||||
<span className="sr-only">Delete Admin </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
data-tooltip-id="change-status"
|
||||
data-tooltip-content="Change status"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-yellow-dark"
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsChangeStatusModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
{..._TooltipDefaultParams({ id: "change-status" })}
|
||||
/>
|
||||
<span className="sr-only">Change Admin Status</span>
|
||||
<UserSettingIcon />
|
||||
</button>
|
||||
<button
|
||||
data-tooltip-id="edit"
|
||||
data-tooltip-content="Edit"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-primary"
|
||||
onClick={() => router.push(`/admins/edit/${item.id}`)}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
|
||||
<span className="sr-only">Edit Admin </span>
|
||||
<PencilSquareIcon />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item?.username}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
<Status status={item?.status ?? ""}>{item?.status}</Status>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
data-tooltip-id="delete"
|
||||
data-tooltip-content="Delete"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
|
||||
<span className="sr-only">Delete Admin </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
data-tooltip-id="change-status"
|
||||
data-tooltip-content="Change status"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-yellow-dark"
|
||||
onClick={() => {
|
||||
setCurrentUser(item);
|
||||
setIsChangeStatusModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
{..._TooltipDefaultParams({ id: "change-status" })}
|
||||
/>
|
||||
<span className="sr-only">Change Admin Status</span>
|
||||
<UserSettingIcon />
|
||||
</button>
|
||||
<button
|
||||
data-tooltip-id="edit"
|
||||
data-tooltip-content="Edit"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-primary"
|
||||
onClick={() => router.push(`/admins/edit/${item.id}`)}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
|
||||
<span className="sr-only">Edit Admin </span>
|
||||
<PencilSquareIcon />
|
||||
</button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Modal
|
||||
@@ -143,6 +158,23 @@ const AdminsTable = () => {
|
||||
setStatus={setStatus}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={isFilterModalOpen}
|
||||
onClose={() => setIsFilterModalOpen(false)}
|
||||
classNames="w-full xl:w-1/2"
|
||||
showButtons={false}
|
||||
>
|
||||
<AdminListFilters
|
||||
setValue={setFilterValue}
|
||||
filterRegister={filterRegister}
|
||||
handleFilterSubmit={handleFilterSubmit}
|
||||
isMutating={isMutating as number}
|
||||
search={search}
|
||||
setIsFilterModalOpen={setIsFilterModalOpen}
|
||||
watch={watch}
|
||||
/>
|
||||
</Modal>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
@@ -3,23 +3,36 @@
|
||||
import { apiDefaultParams } from "@/constants/Api_Params";
|
||||
import { AdminStatus } from "@/constants/enums";
|
||||
import {
|
||||
useAdminListInfiniteQuery,
|
||||
useChangeAdminStatus,
|
||||
useDeleteAdmin,
|
||||
useGetAdminListQuery,
|
||||
} from "@/hooks/queries";
|
||||
import { IUser, TChangeAdminStatusCredentials } from "@/lib/api";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { deleteEmptyKeys } from "@/utils/shared";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const useAdminsPresenter = () => {
|
||||
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false);
|
||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<IUser | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [status, setStatus] = useState<string>(
|
||||
currentUser?.status ?? AdminStatus.ACTIVE,
|
||||
);
|
||||
const isMutating = useIsMutating();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const {
|
||||
register: filterRegister,
|
||||
handleSubmit: handleFilterSubmit,
|
||||
reset: filterFormReset,
|
||||
watch,
|
||||
setValue: setFilterValue,
|
||||
} = useForm();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
@@ -30,11 +43,22 @@ export const useAdminsPresenter = () => {
|
||||
status: currentUser?.status,
|
||||
},
|
||||
});
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
...apiDefaultParams,
|
||||
});
|
||||
const [params, setParams] = useState<Record<string, any>>({});
|
||||
const pathName = usePathname();
|
||||
const { data, isPending } = useAdminListInfiniteQuery(params);
|
||||
|
||||
useEffect(() => {
|
||||
const newParams: Record<string, any> = {
|
||||
...apiDefaultParams,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
};
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
newParams[key] = value;
|
||||
}
|
||||
setParams(newParams);
|
||||
filterFormReset(newParams);
|
||||
}, [searchParams, filterFormReset]);
|
||||
const { data, isPending } = useGetAdminListQuery(params);
|
||||
const { mutate: deleteAdmin } = useDeleteAdmin(() => {
|
||||
setIsModalOpen(false);
|
||||
});
|
||||
@@ -50,23 +74,35 @@ export const useAdminsPresenter = () => {
|
||||
setIsChangeStatusModalOpen(false);
|
||||
},
|
||||
});
|
||||
const allUsers = data?.pages.flatMap((page) => page.data.users) ?? [];
|
||||
|
||||
const search = (data: any) => {
|
||||
const newParams = { ...params, ...data, offset: 0 };
|
||||
const cleanedParams = deleteEmptyKeys(newParams);
|
||||
const queryString = new URLSearchParams(cleanedParams).toString();
|
||||
router.push(`${pathName}?${queryString}`);
|
||||
setIsFilterModalOpen(false);
|
||||
};
|
||||
const allUsers = data?.data?.users ?? [];
|
||||
const metadata = data?.meta;
|
||||
const columns = [
|
||||
"row",
|
||||
"full name",
|
||||
"email",
|
||||
"role",
|
||||
|
||||
"username",
|
||||
"status",
|
||||
"actions",
|
||||
];
|
||||
return {
|
||||
allUsers,
|
||||
setFilterValue,
|
||||
metadata,
|
||||
columns,
|
||||
isPending,
|
||||
onConfirmDelete,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
isMutating,
|
||||
router,
|
||||
setCurrentUser,
|
||||
pathName,
|
||||
@@ -79,5 +115,12 @@ export const useAdminsPresenter = () => {
|
||||
register,
|
||||
errors,
|
||||
handleSubmit,
|
||||
isFilterModalOpen,
|
||||
setIsFilterModalOpen,
|
||||
setParams,
|
||||
filterRegister,
|
||||
handleFilterSubmit,
|
||||
search,
|
||||
watch,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import { Table, TableBody, TableCell, TableHead } from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import { unixToDate } from "@/utils/shared";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { TableHeader, TableRow } from "../../ui/table";
|
||||
import useCmsTablePresenter from "./useCmsTablePresenter";
|
||||
|
||||
const CmsTable = () => {
|
||||
const {
|
||||
columns,
|
||||
data,
|
||||
isPending,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
currentCms,
|
||||
setCurrentCms,
|
||||
onConfirmDelete,
|
||||
router,
|
||||
pathname,
|
||||
} = useCmsTablePresenter();
|
||||
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">
|
||||
<ListHeader />
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column} colSpan={100}>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
<EmptyListWrapper
|
||||
list={data?.data.cms ?? []}
|
||||
columns={columns}
|
||||
emptyMessage="No Cms were found"
|
||||
>
|
||||
{data?.data.cms.map((item, index) => (
|
||||
<TableRow
|
||||
key={item?.id || index}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{item.key}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{unixToDate({ unix: item.created_at, hasTime: true })}
|
||||
</TableCell>
|
||||
<TableCell className="text-start xl:pr-7.5">
|
||||
<div className="flex items-center justify-start gap-x-3.5">
|
||||
<button
|
||||
data-tooltip-id="delete"
|
||||
data-tooltip-content="Delete"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
setCurrentCms(item);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
|
||||
<span className="sr-only">Delete Marketing </span>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
<button
|
||||
data-tooltip-id="edit"
|
||||
data-tooltip-content="Edit"
|
||||
data-tooltip-place="top"
|
||||
className="hover:text-primary"
|
||||
onClick={() => router.push(`${pathname}/edit/${item.id}`)}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
|
||||
<span className="sr-only">Edit Marketing </span>
|
||||
<PencilSquareIcon />
|
||||
</button>
|
||||
{/* <button
|
||||
data-tooltip-id="details"
|
||||
data-tooltip-content="Details"
|
||||
data-tooltip-place="top"
|
||||
onClick={() => {
|
||||
router.push(`${pathname}/details/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<Tooltip {..._TooltipDefaultParams({ id: "details" })} />
|
||||
<EyeIcon />
|
||||
</button> */}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</EmptyListWrapper>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
title={`Deleting ${currentCms?.key}`}
|
||||
message={`Are you sure?`}
|
||||
variant={"default"}
|
||||
size={"md"}
|
||||
confirmText={"Delete"}
|
||||
cancelText={"Cancel"}
|
||||
onConfirm={() => {
|
||||
onConfirmDelete();
|
||||
}}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CmsTable;
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
import { useDeleteCms, useGetCmsList } from "@/hooks/queries/useCmsQueries";
|
||||
import { TCms } from "@/lib/api/types/TCms";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
const useCmsTablePresenter = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [currentCms, setCurrentCms] = useState<TCms | null>(null);
|
||||
const columns = ["row", "key", "created at", "actions"];
|
||||
const { isPending, data } = useGetCmsList();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { mutate: deleteCms } = useDeleteCms(() => {
|
||||
setIsModalOpen(false);
|
||||
setCurrentCms(null);
|
||||
});
|
||||
const onConfirmDelete = () => {
|
||||
if (!currentCms?.id) return;
|
||||
deleteCms(currentCms.id);
|
||||
};
|
||||
return {
|
||||
columns,
|
||||
isPending,
|
||||
data,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
currentCms,
|
||||
setCurrentCms,
|
||||
onConfirmDelete,
|
||||
router,
|
||||
pathname,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCmsTablePresenter;
|
||||
@@ -21,9 +21,7 @@ import Link from "next/link";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const CompaniesTable = ({}: IProps) => {
|
||||
const CompaniesTable = () => {
|
||||
const {
|
||||
isPending,
|
||||
pathName,
|
||||
@@ -47,7 +45,7 @@ const CompaniesTable = ({}: IProps) => {
|
||||
{columns.map((columns) => (
|
||||
<TableHead
|
||||
colSpan={100}
|
||||
className="text-start font-extrabold uppercase"
|
||||
className="text-start uppercase"
|
||||
key={columns}
|
||||
>
|
||||
{columns}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { useContactUsListPresenter } from "./useContactUsListPresenter";
|
||||
|
||||
const ContactUsTable = () => {
|
||||
const { columns, pathname, router, isPending, data } =
|
||||
useContactUsListPresenter();
|
||||
return (
|
||||
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((columns) => (
|
||||
<TableHead
|
||||
colSpan={100}
|
||||
className="text-start uppercase"
|
||||
key={columns}
|
||||
>
|
||||
{columns}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{isPending && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
<EmptyListWrapper
|
||||
list={data ?? []}
|
||||
columns={columns}
|
||||
emptyMessage="No contact us is submitted"
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell><div>hjgfjhg</div></TableCell>
|
||||
</TableRow>
|
||||
</EmptyListWrapper>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactUsTable;
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
import { useReadAllContactUs } from "@/hooks/queries/useContactUsQuery";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
export const useContactUsListPresenter = () => {
|
||||
const columns: string[] = ["row", "actions"];
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { data, isPending } = useReadAllContactUs();
|
||||
return {
|
||||
columns,
|
||||
pathname,
|
||||
router,
|
||||
isPending,
|
||||
data,
|
||||
};
|
||||
};
|
||||
@@ -154,25 +154,27 @@ const CustomersTable = () => {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Modal
|
||||
onConfirm={() => {
|
||||
if (!selectedCompanies || !currentCustomer) {
|
||||
toast.error("Invalid credentials");
|
||||
return;
|
||||
}
|
||||
assignSelectedCompanies({
|
||||
credentials: selectedCompanies,
|
||||
id: currentCustomer?.id ?? "",
|
||||
});
|
||||
}}
|
||||
isOpen={isAssignCompanyModalOpen}
|
||||
onClose={() => setIsAssignCompanyModalOpen(false)}
|
||||
>
|
||||
<AssignToCompanyModalContent
|
||||
setCompanies={setSelectedCompanies}
|
||||
defaultValues={selectedCompanies}
|
||||
/>
|
||||
</Modal>
|
||||
<div className="w-1/12">
|
||||
<Modal
|
||||
onConfirm={() => {
|
||||
if (!selectedCompanies || !currentCustomer) {
|
||||
toast.error("Invalid credentials");
|
||||
return;
|
||||
}
|
||||
assignSelectedCompanies({
|
||||
credentials: selectedCompanies,
|
||||
id: currentCustomer?.id ?? "",
|
||||
});
|
||||
}}
|
||||
isOpen={isAssignCompanyModalOpen}
|
||||
onClose={() => setIsAssignCompanyModalOpen(false)}
|
||||
>
|
||||
<AssignToCompanyModalContent
|
||||
setCompanies={setSelectedCompanies}
|
||||
defaultValues={selectedCompanies}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import {
|
||||
FieldValues,
|
||||
UseFormHandleSubmit,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
UseFormWatch,
|
||||
} from "react-hook-form";
|
||||
|
||||
interface NotificationHistoryListFiltersProps {
|
||||
filterRegister: UseFormRegister<FieldValues>;
|
||||
setIsFilterModalOpen: (isOpen: boolean) => void;
|
||||
isMutating: number;
|
||||
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
|
||||
search: (data: any) => void;
|
||||
watch: UseFormWatch<FieldValues>;
|
||||
setValue: UseFormSetValue<any>;
|
||||
}
|
||||
|
||||
const NotificationHistoryListFilters = ({
|
||||
filterRegister,
|
||||
setIsFilterModalOpen,
|
||||
isMutating,
|
||||
handleFilterSubmit,
|
||||
search,
|
||||
}: NotificationHistoryListFiltersProps) => {
|
||||
return (
|
||||
<form
|
||||
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
|
||||
onSubmit={handleFilterSubmit(search)}
|
||||
>
|
||||
<Select
|
||||
{...filterRegister("seen")}
|
||||
items={[
|
||||
{ label: "Seen", value: "true" },
|
||||
{ label: "Not seen", value: "false" },
|
||||
]}
|
||||
label="Seen"
|
||||
name="seen"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("search")}
|
||||
name="search"
|
||||
label="search"
|
||||
placeholder="search"
|
||||
type="search"
|
||||
/>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-primary p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||
>
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
<span>Search</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium capitalize text-white hover:bg-opacity-90"
|
||||
onClick={() => setIsFilterModalOpen(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationHistoryListFilters;
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import { truncateString, unixToDate } from "@/utils/shared";
|
||||
import Link from "next/link";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
import Boolean from "@/components/ui/Boolean";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import NotificationHistoryListFilters from "./NotificationHistoryListFilters";
|
||||
import useNotificationHistoryTablePresenter from "./useNotificationHistoryTablePresenter";
|
||||
|
||||
const NotificationHistoryTable = () => {
|
||||
@@ -28,93 +30,122 @@ const NotificationHistoryTable = () => {
|
||||
notificationHistory,
|
||||
pathName,
|
||||
router,
|
||||
isFilterModalOpen,
|
||||
setIsFilterModalOpen,
|
||||
setParams,
|
||||
handleSubmit,
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
search,
|
||||
isMutating,
|
||||
} = useNotificationHistoryTablePresenter();
|
||||
|
||||
return (
|
||||
<ShowcaseSection className="flex flex-col">
|
||||
<button className="mb-5 flex w-fit justify-center self-end rounded-lg bg-primary p-[13px] font-medium text-white hover:bg-opacity-90">
|
||||
<Link href={`${pathName}/create`}>Create Notification </Link>
|
||||
</button>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead colSpan={100} key={column}>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{isLoading && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{!notificationHistory?.length ? (
|
||||
<>
|
||||
<ShowcaseSection className="flex flex-col">
|
||||
<ListHeader
|
||||
createButtonText="Create notification"
|
||||
onFilter={() => setIsFilterModalOpen(true)}
|
||||
/>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<h2>No notifications</h2>
|
||||
</TableCell>
|
||||
{columns.map((column) => (
|
||||
<TableHead colSpan={100} key={column}>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
) : (
|
||||
notificationHistory.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell colSpan={100}>{index + 1}</TableCell>
|
||||
<TableCell colSpan={100}>{item.title}</TableCell>
|
||||
<TableCell colSpan={100}>{item.event_type}</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{unixToDate({
|
||||
hasTime: true,
|
||||
unix: item.created_at,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100} className="truncate">
|
||||
{truncateString(item.message, 20, 20)}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize" colSpan={100}>
|
||||
{item.priority}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize" colSpan={100}>
|
||||
{item.type}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize" colSpan={100}>
|
||||
<Boolean value={item.seen} />
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<Status status={item.status}>{item.status}</Status>
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<button
|
||||
data-tooltip-id="details"
|
||||
data-tooltip-content="Details"
|
||||
data-tooltip-place="top"
|
||||
onClick={() => {
|
||||
router.push(`${pathName}/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
{..._TooltipDefaultParams({
|
||||
id: "details",
|
||||
})}
|
||||
/>
|
||||
<EyeIcon />
|
||||
</button>
|
||||
</TableHeader>
|
||||
{isLoading && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{!notificationHistory?.length ? (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<h2>No notifications</h2>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination
|
||||
currentPage={metadata?.page ? metadata?.page - 1 : 0}
|
||||
totalPages={metadata?.pages ?? 1}
|
||||
onPageChange={(e) => {
|
||||
setParams((currentParams) => ({
|
||||
...currentParams,
|
||||
offset: e.selected * (metadata?.limit ?? 10),
|
||||
limit: metadata?.limit ?? 10,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</ShowcaseSection>
|
||||
) : (
|
||||
notificationHistory.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell colSpan={100}>{index + 1}</TableCell>
|
||||
<TableCell colSpan={100}>{item.title}</TableCell>
|
||||
<TableCell colSpan={100}>{item.event_type}</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{unixToDate({
|
||||
hasTime: true,
|
||||
unix: item.created_at,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
{item.recipient?.full_name}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100} className="truncate">
|
||||
{truncateString(item.message, 20, 20)}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize" colSpan={100}>
|
||||
{item.priority}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize" colSpan={100}>
|
||||
{item.type}
|
||||
</TableCell>
|
||||
<TableCell className="capitalize" colSpan={100}>
|
||||
<Boolean value={item.seen} />
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<Status status={item.status}>{item.status}</Status>
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>
|
||||
<button
|
||||
data-tooltip-id="details"
|
||||
data-tooltip-content="Details"
|
||||
data-tooltip-place="top"
|
||||
onClick={() => {
|
||||
router.push(`${pathName}/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
{..._TooltipDefaultParams({
|
||||
id: "details",
|
||||
})}
|
||||
/>
|
||||
<EyeIcon />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination
|
||||
currentPage={metadata?.page ? metadata?.page - 1 : 0}
|
||||
totalPages={metadata?.pages ?? 1}
|
||||
onPageChange={(e) => {
|
||||
setParams((currentParams) => ({
|
||||
...currentParams,
|
||||
offset: e.selected * (metadata?.limit ?? 10),
|
||||
limit: metadata?.limit ?? 10,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</ShowcaseSection>
|
||||
<Modal
|
||||
isOpen={isFilterModalOpen}
|
||||
onClose={() => setIsFilterModalOpen(false)}
|
||||
showButtons={false}
|
||||
>
|
||||
<NotificationHistoryListFilters
|
||||
filterRegister={register}
|
||||
handleFilterSubmit={handleSubmit}
|
||||
isMutating={isMutating}
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
setIsFilterModalOpen={setIsFilterModalOpen}
|
||||
search={search}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
"use client";
|
||||
import { apiDefaultParams } from "@/constants/Api_Params";
|
||||
import { useGetNotificationHistoryQuery } from "@/hooks/queries/useNotificationQueries";
|
||||
import { deleteEmptyKeys } from "@/utils/shared";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
const useNotificationHistoryTablePresenter = () => {
|
||||
const { register, handleSubmit, watch, setValue } = useForm();
|
||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||
const [params, setParams] = useState<Record<string, any>>({
|
||||
...apiDefaultParams,
|
||||
});
|
||||
const { data, isLoading } = useGetNotificationHistoryQuery(params);
|
||||
|
||||
const isMutating = useIsMutating();
|
||||
const columns: string[] = [
|
||||
"row",
|
||||
"title",
|
||||
"channel",
|
||||
"created at",
|
||||
"receiver",
|
||||
"message",
|
||||
"priority",
|
||||
"type",
|
||||
@@ -24,6 +30,14 @@ const useNotificationHistoryTablePresenter = () => {
|
||||
];
|
||||
const pathName = usePathname();
|
||||
const router = useRouter();
|
||||
const search = (data: any) => {
|
||||
const newParams = { ...params, ...data, offset: 0 };
|
||||
const cleanedParams = deleteEmptyKeys(newParams);
|
||||
setParams(cleanedParams);
|
||||
const queryString = new URLSearchParams(cleanedParams).toString();
|
||||
router.push(`${pathName}?${queryString}`);
|
||||
setIsFilterModalOpen(false);
|
||||
};
|
||||
return {
|
||||
notificationHistory: data?.data,
|
||||
metadata: data?.meta,
|
||||
@@ -32,6 +46,14 @@ const useNotificationHistoryTablePresenter = () => {
|
||||
router,
|
||||
pathName,
|
||||
setParams,
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
isFilterModalOpen,
|
||||
setIsFilterModalOpen,
|
||||
isMutating,
|
||||
search,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ type ButtonProps = HTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
type?: "button" | "submit" | "reset";
|
||||
};
|
||||
|
||||
export function Button({
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import { FilterIcon } from "@/assets/icons";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import IsVisible from "./IsVisible";
|
||||
|
||||
interface IProps {
|
||||
hasCreate?: boolean;
|
||||
hasFilter?: boolean;
|
||||
createButtonText?: string;
|
||||
filterButtonText?: string;
|
||||
onFilter?: () => void;
|
||||
}
|
||||
|
||||
const ListHeader = ({
|
||||
hasCreate = true,
|
||||
hasFilter = true,
|
||||
createButtonText = "Create",
|
||||
filterButtonText = "Filter",
|
||||
onFilter,
|
||||
}: IProps) => {
|
||||
const pathName = usePathname();
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<IsVisible condition={hasFilter}>
|
||||
<button
|
||||
onClick={onFilter}
|
||||
className="mb-5 flex w-fit justify-center self-end rounded-2xl border bg-gray-5 bg-opacity-20 px-6 py-2 font-medium transition duration-300 hover:bg-opacity-30 dark:border-gray-7 dark:bg-black dark:bg-opacity-40 dark:text-white dark:hover:bg-opacity-90"
|
||||
>
|
||||
<span>
|
||||
<FilterIcon />
|
||||
</span>
|
||||
{filterButtonText}
|
||||
</button>
|
||||
</IsVisible>
|
||||
<IsVisible condition={hasCreate}>
|
||||
<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`}>{createButtonText}</Link>
|
||||
</button>
|
||||
</IsVisible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListHeader;
|
||||
@@ -0,0 +1,154 @@
|
||||
import { CheckIcon } from "@/assets/icons";
|
||||
import React, { ReactNode, useState } from "react";
|
||||
export interface Step {
|
||||
title: string;
|
||||
description?: string;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
interface StepperProps {
|
||||
steps: Step[];
|
||||
currentStep?: number;
|
||||
onStepChange?: (step: number) => void;
|
||||
onSubmit?: () => void;
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
const Stepper: React.FC<StepperProps> = ({
|
||||
steps,
|
||||
currentStep: controlledStep,
|
||||
onStepChange,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
}) => {
|
||||
const [internalStep, setInternalStep] = useState(0);
|
||||
const currentStep =
|
||||
controlledStep !== undefined ? controlledStep : internalStep;
|
||||
|
||||
const handleStepClick = (index: number) => {
|
||||
if (index <= currentStep) {
|
||||
if (onStepChange) {
|
||||
onStepChange(index);
|
||||
} else {
|
||||
setInternalStep(index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep === steps.length - 1) {
|
||||
// On last step, trigger submit
|
||||
if (onSubmit) {
|
||||
onSubmit();
|
||||
}
|
||||
} else if (currentStep < steps.length - 1) {
|
||||
const newStep = currentStep + 1;
|
||||
if (onStepChange) {
|
||||
onStepChange(newStep);
|
||||
} else {
|
||||
setInternalStep(newStep);
|
||||
}
|
||||
}
|
||||
if (window) {
|
||||
window.scroll({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep > 0) {
|
||||
const newStep = currentStep - 1;
|
||||
if (onStepChange) {
|
||||
onStepChange(newStep);
|
||||
} else {
|
||||
setInternalStep(newStep);
|
||||
}
|
||||
}
|
||||
if (window) {
|
||||
window.scroll({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-4xl p-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
{steps.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div className="flex flex-1 flex-col items-center">
|
||||
<button
|
||||
onClick={() => handleStepClick(index)}
|
||||
disabled={index > currentStep}
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-full font-semibold transition-all duration-200 ${
|
||||
index < currentStep
|
||||
? "cursor-pointer bg-primary text-white hover:bg-primary/90"
|
||||
: index === currentStep
|
||||
? "bg-primary text-white ring-4 ring-primary/20"
|
||||
: "cursor-not-allowed bg-gray-2 text-gray-5"
|
||||
}`}
|
||||
>
|
||||
{index < currentStep ? (
|
||||
<CheckIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<span>{index + 1}</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="mt-2 text-center">
|
||||
<div
|
||||
className={`text-sm font-medium ${
|
||||
index <= currentStep ? "text-dark-2" : "text-gray-4"
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
</div>
|
||||
{step.description && (
|
||||
<div className="mt-1 text-xs text-gray-5">
|
||||
{step.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={`mx-4 -mt-12 h-1 flex-1 transition-all duration-300 ${
|
||||
index < currentStep ? "bg-primary" : "bg-stroke"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>{steps[currentStep].content}</div>
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
onClick={prevStep}
|
||||
disabled={currentStep === 0}
|
||||
className={`rounded-lg px-6 py-2 font-medium transition-all ${
|
||||
currentStep === 0
|
||||
? "cursor-not-allowed bg-gray-1 text-gray-4"
|
||||
: "bg-gray-2 text-gray-7 hover:bg-gray-3"
|
||||
}`}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={nextStep}
|
||||
disabled={isSubmitting}
|
||||
className={`rounded-lg px-6 py-2 font-medium transition-all ${
|
||||
isSubmitting
|
||||
? "cursor-not-allowed bg-gray-1 text-gray-4"
|
||||
: "bg-primary text-white hover:bg-primary/90"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting
|
||||
? "Submitting..."
|
||||
: currentStep === steps.length - 1
|
||||
? "Submit"
|
||||
: "Next"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stepper;
|
||||
@@ -12,6 +12,7 @@ interface ModalProps {
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
showButtons?: boolean;
|
||||
classNames?: string;
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
@@ -22,6 +23,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
confirmText = "confirm",
|
||||
cancelText = "close",
|
||||
showButtons = true,
|
||||
classNames,
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const isMutating = useIsMutating();
|
||||
@@ -49,7 +51,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="max-h-[90vh] min-w-75 max-w-[90vw] overflow-auto rounded-lg bg-white p-5 shadow-4 dark:bg-dark-2"
|
||||
className={`max-h-[90vh] max-w-[90vw] overflow-auto rounded-lg bg-white p-5 shadow-4 dark:bg-dark-2 ${classNames}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
export const Countries = [
|
||||
{ value: "AF", label: "Afghanistan" },
|
||||
{ value: "AX", label: "Åland Islands" },
|
||||
{ value: "AL", label: "Albania" },
|
||||
{ value: "DZ", label: "Algeria" },
|
||||
{ value: "AS", label: "American Samoa" },
|
||||
{ value: "AD", label: "Andorra" },
|
||||
{ value: "AO", label: "Angola" },
|
||||
{ value: "AI", label: "Anguilla" },
|
||||
{ value: "AQ", label: "Antarctica" },
|
||||
{ value: "AG", label: "Antigua and Barbuda" },
|
||||
{ value: "AR", label: "Argentina" },
|
||||
{ value: "AM", label: "Armenia" },
|
||||
{ value: "AW", label: "Aruba" },
|
||||
{ value: "AU", label: "Australia" },
|
||||
{ value: "AT", label: "Austria" },
|
||||
{ value: "AZ", label: "Azerbaijan" },
|
||||
{ value: "BS", label: "Bahamas (The)" },
|
||||
{ value: "BH", label: "Bahrain" },
|
||||
{ value: "BD", label: "Bangladesh" },
|
||||
{ value: "BB", label: "Barbados" },
|
||||
{ value: "BY", label: "Belarus" },
|
||||
{ value: "BE", label: "Belgium" },
|
||||
{ value: "BZ", label: "Belize" },
|
||||
{ value: "BJ", label: "Benin" },
|
||||
{ value: "BM", label: "Bermuda" },
|
||||
{ value: "BT", label: "Bhutan" },
|
||||
{ value: "BO", label: "Bolivia (Plurinational State of)" },
|
||||
{ value: "BQ", label: "Bonaire, Sint Eustatius and Saba" },
|
||||
{ value: "BA", label: "Bosnia and Herzegovina" },
|
||||
{ value: "BW", label: "Botswana" },
|
||||
{ value: "BV", label: "Bouvet Island" },
|
||||
{ value: "BR", label: "Brazil" },
|
||||
{ value: "IO", label: "British Indian Ocean Territory" },
|
||||
{ value: "BN", label: "Brunei Darussalam" },
|
||||
{ value: "BG", label: "Bulgaria" },
|
||||
{ value: "BF", label: "Burkina Faso" },
|
||||
{ value: "BI", label: "Burundi" },
|
||||
{ value: "CV", label: "Cabo Verde" },
|
||||
{ value: "KH", label: "Cambodia" },
|
||||
{ value: "CM", label: "Cameroon" },
|
||||
{ value: "CA", label: "Canada" },
|
||||
{ value: "KY", label: "Cayman Islands" },
|
||||
{ value: "CF", label: "Central African Republic" },
|
||||
{ value: "TD", label: "Chad" },
|
||||
{ value: "CL", label: "Chile" },
|
||||
{ value: "CN", label: "China" },
|
||||
{ value: "CX", label: "Christmas Island" },
|
||||
{ value: "CC", label: "Cocos (Keeling) Islands" },
|
||||
{ value: "CO", label: "Colombia" },
|
||||
{ value: "KM", label: "Comoros" },
|
||||
{ value: "CG", label: "Congo (Republic of the)" },
|
||||
{ value: "CD", label: "Congo (Democratic Republic of the)" },
|
||||
{ value: "CK", label: "Cook Islands" },
|
||||
{ value: "CR", label: "Costa Rica" },
|
||||
{ value: "CI", label: "Côte d’Ivoire" },
|
||||
{ value: "HR", label: "Croatia" },
|
||||
{ value: "CU", label: "Cuba" },
|
||||
{ value: "CW", label: "Curaçao" },
|
||||
{ value: "CY", label: "Cyprus" },
|
||||
{ value: "CZ", label: "Czechia" },
|
||||
{ value: "DK", label: "Denmark" },
|
||||
{ value: "DJ", label: "Djibouti" },
|
||||
{ value: "DM", label: "Dominica" },
|
||||
{ value: "DO", label: "Dominican Republic" },
|
||||
{ value: "EC", label: "Ecuador" },
|
||||
{ value: "EG", label: "Egypt" },
|
||||
{ value: "SV", label: "El Salvador" },
|
||||
{ value: "GQ", label: "Equatorial Guinea" },
|
||||
{ value: "ER", label: "Eritrea" },
|
||||
{ value: "EE", label: "Estonia" },
|
||||
{ value: "SZ", label: "Eswatini" },
|
||||
{ value: "ET", label: "Ethiopia" },
|
||||
{ value: "FK", label: "Falkland Islands (Malvinas)" },
|
||||
{ value: "FO", label: "Faroe Islands" },
|
||||
{ value: "FJ", label: "Fiji" },
|
||||
{ value: "FI", label: "Finland" },
|
||||
{ value: "FR", label: "France" },
|
||||
{ value: "GF", label: "French Guiana" },
|
||||
{ value: "PF", label: "French Polynesia" },
|
||||
{ value: "TF", label: "French Southern Territories" },
|
||||
{ value: "GA", label: "Gabon" },
|
||||
{ value: "GM", label: "Gambia (The)" },
|
||||
{ value: "GE", label: "Georgia" },
|
||||
{ value: "DE", label: "Germany" },
|
||||
{ value: "GH", label: "Ghana" },
|
||||
{ value: "GI", label: "Gibraltar" },
|
||||
{ value: "GR", label: "Greece" },
|
||||
{ value: "GL", label: "Greenland" },
|
||||
{ value: "GD", label: "Grenada" },
|
||||
{ value: "GP", label: "Guadeloupe" },
|
||||
{ value: "GU", label: "Guam" },
|
||||
{ value: "GT", label: "Guatemala" },
|
||||
{ value: "GG", label: "Guernsey" },
|
||||
{ value: "GN", label: "Guinea" },
|
||||
{ value: "GW", label: "Guinea-Bissau" },
|
||||
{ value: "GY", label: "Guyana" },
|
||||
{ value: "HT", label: "Haiti" },
|
||||
{ value: "HM", label: "Heard Island and McDonald Islands" },
|
||||
{ value: "VA", label: "Holy See (Vatican City State)" },
|
||||
{ value: "HN", label: "Honduras" },
|
||||
{ value: "HK", label: "Hong Kong" },
|
||||
{ value: "HU", label: "Hungary" },
|
||||
{ value: "IS", label: "Iceland" },
|
||||
{ value: "IN", label: "India" },
|
||||
{ value: "ID", label: "Indonesia" },
|
||||
{ value: "IR", label: "Iran" },
|
||||
{ value: "IQ", label: "Iraq" },
|
||||
{ value: "IE", label: "Ireland" },
|
||||
{ value: "IM", label: "Isle of Man" },
|
||||
{ value: "IL", label: "Israel" },
|
||||
{ value: "IT", label: "Italy" },
|
||||
{ value: "JM", label: "Jamaica" },
|
||||
{ value: "JP", label: "Japan" },
|
||||
{ value: "JE", label: "Jersey" },
|
||||
{ value: "JO", label: "Jordan" },
|
||||
{ value: "KZ", label: "Kazakhstan" },
|
||||
{ value: "KE", label: "Kenya" },
|
||||
{ value: "KI", label: "Kiribati" },
|
||||
{ value: "KP", label: "Korea (Democratic People’s Republic of)" },
|
||||
{ value: "KR", label: "Korea (Republic of)" },
|
||||
{ value: "KW", label: "Kuwait" },
|
||||
{ value: "KG", label: "Kyrgyzstan" },
|
||||
{ value: "LA", label: "Lao People’s Democratic Republic" },
|
||||
{ value: "LV", label: "Latvia" },
|
||||
{ value: "LB", label: "Lebanon" },
|
||||
{ value: "LS", label: "Lesotho" },
|
||||
{ value: "LR", label: "Liberia" },
|
||||
{ value: "LY", label: "Libya" },
|
||||
{ value: "LI", label: "Liechtenstein" },
|
||||
{ value: "LT", label: "Lithuania" },
|
||||
{ value: "LU", label: "Luxembourg" },
|
||||
{ value: "MO", label: "Macao" },
|
||||
{ value: "MG", label: "Madagascar" },
|
||||
{ value: "MW", label: "Malawi" },
|
||||
{ value: "MY", label: "Malaysia" },
|
||||
{ value: "MV", label: "Maldives" },
|
||||
{ value: "ML", label: "Mali" },
|
||||
{ value: "MT", label: "Malta" },
|
||||
{ value: "MH", label: "Marshall Islands" },
|
||||
{ value: "MQ", label: "Martinique" },
|
||||
{ value: "MR", label: "Mauritania" },
|
||||
{ value: "MU", label: "Mauritius" },
|
||||
{ value: "YT", label: "Mayotte" },
|
||||
{ value: "MX", label: "Mexico" },
|
||||
{ value: "FM", label: "Micronesia (Federated States of)" },
|
||||
{ value: "MD", label: "Moldova (Republic of)" },
|
||||
{ value: "MC", label: "Monaco" },
|
||||
{ value: "MN", label: "Mongolia" },
|
||||
{ value: "ME", label: "Montenegro" },
|
||||
{ value: "MS", label: "Montserrat" },
|
||||
{ value: "MA", label: "Morocco" },
|
||||
{ value: "MZ", label: "Mozambique" },
|
||||
{ value: "MM", label: "Myanmar" },
|
||||
{ value: "NA", label: "Namibia" },
|
||||
{ value: "NR", label: "Nauru" },
|
||||
{ value: "NP", label: "Nepal" },
|
||||
{ value: "NL", label: "Netherlands (The)" },
|
||||
{ value: "NC", label: "New Caledonia" },
|
||||
{ value: "NZ", label: "New Zealand" },
|
||||
{ value: "NI", label: "Nicaragua" },
|
||||
{ value: "NE", label: "Niger (The)" },
|
||||
{ value: "NG", label: "Nigeria" },
|
||||
{ value: "NU", label: "Niue" },
|
||||
{ value: "NF", label: "Norfolk Island" },
|
||||
{ value: "MK", label: "North Macedonia" },
|
||||
{ value: "MP", label: "Northern Mariana Islands" },
|
||||
{ value: "NO", label: "Norway" },
|
||||
{ value: "OM", label: "Oman" },
|
||||
{ value: "PK", label: "Pakistan" },
|
||||
{ value: "PW", label: "Palau" },
|
||||
{ value: "PS", label: "Palestine, State of" },
|
||||
{ value: "PA", label: "Panama" },
|
||||
{ value: "PG", label: "Papua New Guinea" },
|
||||
{ value: "PY", label: "Paraguay" },
|
||||
{ value: "PE", label: "Peru" },
|
||||
{ value: "PH", label: "Philippines (The)" },
|
||||
{ value: "PN", label: "Pitcairn" },
|
||||
{ value: "PL", label: "Poland" },
|
||||
{ value: "PT", label: "Portugal" },
|
||||
{ value: "PR", label: "Puerto Rico" },
|
||||
{ value: "QA", label: "Qatar" },
|
||||
{ value: "RE", label: "Réunion" },
|
||||
{ value: "RO", label: "Romania" },
|
||||
{ value: "RU", label: "Russian Federation (The)" },
|
||||
{ value: "RW", label: "Rwanda" },
|
||||
{ value: "BL", label: "Saint Barthélemy" },
|
||||
{ value: "SH", label: "Saint Helena, Ascension and Tristan da Cunha" },
|
||||
{ value: "KN", label: "Saint Kitts and Nevis" },
|
||||
{ value: "LC", label: "Saint Lucia" },
|
||||
{ value: "MF", label: "Saint Martin (French part)" },
|
||||
{ value: "PM", label: "Saint Pierre and Miquelon" },
|
||||
{ value: "VC", label: "Saint Vincent and the Grenadines" },
|
||||
{ value: "WS", label: "Samoa" },
|
||||
{ value: "SM", label: "San Marino" },
|
||||
{ value: "ST", label: "Sao Tome and Principe" },
|
||||
{ value: "SA", label: "Saudi Arabia" },
|
||||
{ value: "SN", label: "Senegal" },
|
||||
{ value: "RS", label: "Serbia" },
|
||||
{ value: "SC", label: "Seychelles" },
|
||||
{ value: "SL", label: "Sierra Leone" },
|
||||
{ value: "SG", label: "Singapore" },
|
||||
{ value: "SX", label: "Sint Maarten (Dutch part)" },
|
||||
{ value: "SK", label: "Slovakia" },
|
||||
{ value: "SI", label: "Slovenia" },
|
||||
{ value: "SB", label: "Solomon Islands" },
|
||||
{ value: "SO", label: "Somalia" },
|
||||
{ value: "ZA", label: "South Africa" },
|
||||
{ value: "GS", label: "South Georgia and the South Sandwich Islands" },
|
||||
{ value: "SS", label: "South Sudan" },
|
||||
{ value: "ES", label: "Spain" },
|
||||
{ value: "LK", label: "Sri Lanka" },
|
||||
{ value: "SD", label: "Sudan (The)" },
|
||||
{ value: "SR", label: "Suriname" },
|
||||
{ value: "SJ", label: "Svalbard and Jan Mayen" },
|
||||
{ value: "SE", label: "Sweden" },
|
||||
{ value: "CH", label: "Switzerland" },
|
||||
{ value: "SY", label: "Syrian Arab Republic" },
|
||||
{ value: "TW", label: "Taiwan (Province of China)" },
|
||||
{ value: "TJ", label: "Tajikistan" },
|
||||
{ value: "TZ", label: "Tanzania (United Republic of)" },
|
||||
{ value: "TH", label: "Thailand" },
|
||||
{ value: "TL", label: "Timor-Leste" },
|
||||
{ value: "TG", label: "Togo" },
|
||||
{ value: "TK", label: "Tokelau" },
|
||||
{ value: "TO", label: "Tonga" },
|
||||
{ value: "TT", label: "Trinidad and Tobago" },
|
||||
{ value: "TN", label: "Tunisia" },
|
||||
{ value: "TR", label: "Türkiye" },
|
||||
{ value: "TM", label: "Turkmenistan" },
|
||||
{ value: "TC", label: "Turks and Caicos Islands" },
|
||||
{ value: "TV", label: "Tuvalu" },
|
||||
{ value: "UG", label: "Uganda" },
|
||||
{ value: "UA", label: "Ukraine" },
|
||||
{ value: "AE", label: "United Arab Emirates" },
|
||||
{
|
||||
value: "GB",
|
||||
label: "United Kingdom of Great Britain and Northern Ireland",
|
||||
},
|
||||
{ value: "US", label: "United States of America" },
|
||||
{ value: "UM", label: "United States Minor Outlying Islands" },
|
||||
{ value: "UY", label: "Uruguay" },
|
||||
{ value: "UZ", label: "Uzbekistan" },
|
||||
{ value: "VU", label: "Vanuatu" },
|
||||
{ value: "VE", label: "Venezuela (Bolivarian Republic of)" },
|
||||
{ value: "VN", label: "Viet Nam" },
|
||||
{ value: "VG", label: "Virgin Islands (British)" },
|
||||
{ value: "VI", label: "Virgin Islands (U.S.)" },
|
||||
{ value: "WF", label: "Wallis and Futuna" },
|
||||
{
|
||||
value: "EH",
|
||||
label:
|
||||
"Western Sahara (Sahrawi Arab Democratic Republic claimed by Morocco)",
|
||||
},
|
||||
{ value: "YE", label: "Yemen" },
|
||||
{ value: "ZM", label: "Zambia" },
|
||||
{ value: "ZW", label: "Zimbabwe" },
|
||||
];
|
||||
@@ -14,12 +14,9 @@ import React, {
|
||||
} from "react";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { ILoginResponse, IUser, UserSchema } from "../lib/api/types";
|
||||
import { COOKIE_KEYS } from "../lib/shared/cookies";
|
||||
|
||||
|
||||
|
||||
interface UserContextType {
|
||||
user: IUser | null;
|
||||
accessToken: string | null;
|
||||
@@ -27,7 +24,7 @@ interface UserContextType {
|
||||
isLoading: boolean;
|
||||
setAuthState: (data: ILoginResponse) => void;
|
||||
logout: () => void;
|
||||
setUser:Dispatch<SetStateAction<IUser|null>>
|
||||
setUser: Dispatch<SetStateAction<IUser | null>>;
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||
@@ -48,7 +45,7 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
|
||||
Cookies.remove(COOKIE_KEYS.access_token);
|
||||
Cookies.remove(COOKIE_KEYS.refresh_token);
|
||||
Cookies.remove(COOKIE_KEYS.user);
|
||||
router.refresh();
|
||||
window.location.href = "/auth/sign-in";
|
||||
}, [router]);
|
||||
useEffect(() => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
export * from "./useUsersQueries";
|
||||
export * from "./useCompaniesQueries";
|
||||
export * from "./useTenders";
|
||||
export * from "./useCustomerQueries";
|
||||
export * from "./useFeedbackQueries";
|
||||
export * from "./useProfileQueries";
|
||||
export * from "./useFeedbackQueries"
|
||||
export * from "./useTenders";
|
||||
export * from "./useUsersQueries";
|
||||
|
||||
export * from "./useCompaniesQueries";
|
||||
export * from "./useCustomerQueries";
|
||||
export * from "./useFeedbackQueries";
|
||||
export * from "./useFlagsQueries";
|
||||
export * from "./useNotificationQueries";
|
||||
export * from "./useProfileQueries";
|
||||
export * from "./useTenders";
|
||||
export * from "./useUsersQueries";
|
||||
export * from "./useAdminListQuery";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { API_ENDPOINTS, userService } from "@/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useGetAdminListQuery = (params?: Record<string, any>) => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.USER.ADMINS, params], [params]);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => userService.adminsList(params),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { API_ENDPOINTS } from "@/lib/api";
|
||||
import { cmsService } from "@/lib/api/services/cms-service";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useGetCmsList = () => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.CMS.BASE()], []);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: cmsService.getCms,
|
||||
});
|
||||
};
|
||||
export const useGetCmsDetails = (id: string) => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.CMS.BASE(id)], [id]);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => cmsService.getCmsDetails(id),
|
||||
});
|
||||
};
|
||||
export const useCreateCms = () => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const mutationKey = useMemo(
|
||||
() => [API_ENDPOINTS.CMS.BASE(), "Create cms"],
|
||||
[],
|
||||
);
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn: cmsService.createCms,
|
||||
onSuccess: ({ data }: { data: { message: string } }) => {
|
||||
toast.success(data.message);
|
||||
router.push("/marketing");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.CMS.BASE()],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useDeleteCms = (successCallback?: () => void) => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutationKey = useMemo(
|
||||
() => [API_ENDPOINTS.CMS.BASE(), "Delete CMS"],
|
||||
[],
|
||||
);
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn: cmsService.deleteCms,
|
||||
onSuccess: ({ data }: { data: { message: string } }) => {
|
||||
toast.success(data.message);
|
||||
successCallback?.();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.CMS.BASE()],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useUpdateCms = (id: string) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const mutationKey = useMemo(
|
||||
() => [API_ENDPOINTS.CMS.BASE(id), "Update cms"],
|
||||
[id],
|
||||
);
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn: (data: any) => cmsService.updateCms({ id, data }),
|
||||
onSuccess: () => {
|
||||
toast.success("CMS updated successfully");
|
||||
router.push("/marketing");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.CMS.BASE()],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.CMS.BASE(id)],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { API_ENDPOINTS } from "@/lib/api";
|
||||
import { contactUsService } from "@/lib/api/services/contact-us-service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useReadAllContactUs = () => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.CONTACT_US.READ_ALL], []);
|
||||
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => contactUsService.getAllContactUs(),
|
||||
});
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { toast } from "react-toastify";
|
||||
|
||||
export const useGetNotificationHistoryQuery = (
|
||||
params?: Record<string, any>,
|
||||
options?: { enabled: boolean },
|
||||
) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.NOTIFICATIONS.HISTORY, params],
|
||||
@@ -14,6 +15,7 @@ export const useGetNotificationHistoryQuery = (
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => notificationService.getNotifications(params),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
export const useGetMyNotifications = (params?: Record<string, any>) => {
|
||||
|
||||
@@ -3,6 +3,9 @@ export const API_ENDPOINTS = {
|
||||
POSTS: {
|
||||
READ_ALL: "posts ",
|
||||
},
|
||||
CMS: {
|
||||
BASE: (id?: string) => (id ? `cms/${id}` : "cms"),
|
||||
},
|
||||
USER: {
|
||||
LOGIN: "profile/login",
|
||||
LOGOUT: "profile/logout",
|
||||
@@ -58,4 +61,7 @@ export const API_ENDPOINTS = {
|
||||
MY_NOTIFICATIONS: "notifications/my",
|
||||
MARK_AS_SEEN: (id: string) => `notifications/mark-seen/${id}`,
|
||||
},
|
||||
CONTACT_US: {
|
||||
READ_ALL: "contacts",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import { ApiResponse } from "../types";
|
||||
import { TCms, TCmsListResponse } from "../types/TCms";
|
||||
|
||||
export const cmsService = {
|
||||
getCms: async (): Promise<ApiResponse<TCmsListResponse>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.CMS.BASE())).data;
|
||||
} catch (error) {
|
||||
console.error("Error Caught in CMS Service => Get cms", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
createCms: async (data: any) => {
|
||||
try {
|
||||
return (await api.post(API_ENDPOINTS.CMS.BASE(), data)).data;
|
||||
} catch (error) {
|
||||
console.error("Error Caught in CMS Service => Create cms", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getCmsDetails: async (id: string) => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.CMS.BASE(id))).data;
|
||||
} catch (error) {
|
||||
console.error("Error Caught In CMS Service => getCmsDetails: ", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
deleteCms: async (id: string) => {
|
||||
try {
|
||||
return (await api.delete(API_ENDPOINTS.CMS.BASE(id))).data;
|
||||
} catch (error) {
|
||||
console.error("Error caught in CMS Service => delete CMS: ", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
updateCms: async ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: string;
|
||||
data: any;
|
||||
}): Promise<{ data: TCms; message: string }> => {
|
||||
try {
|
||||
return (await api.put(API_ENDPOINTS.CMS.BASE(id), data)).data;
|
||||
} catch (error) {
|
||||
console.error("Error Caught in CMS Service => Update cms", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
|
||||
export const contactUsService = {
|
||||
getAllContactUs: async () => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.CONTACT_US.READ_ALL)).data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR caught in contact-us service => getAllContactUs",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CmsCredentials = z.object({
|
||||
advantages: z.object({
|
||||
cards: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
description: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
challenges: z.object({
|
||||
cards: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
description: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
chart: z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.number(),
|
||||
}),
|
||||
),
|
||||
missedAmount: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
contact: z.object({
|
||||
description: z.string(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
name: z.string(),
|
||||
placeholder: z.string(),
|
||||
required: z.boolean(),
|
||||
}),
|
||||
),
|
||||
submitButtonText: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
features: z.object({
|
||||
cards: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
description: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
footer: z.object({
|
||||
copyright: z.string(),
|
||||
email: z.string(),
|
||||
location: z.string(),
|
||||
phone: z.string(),
|
||||
tagline: z.string(),
|
||||
}),
|
||||
hero: z.object({
|
||||
buttonLink: z.string(),
|
||||
buttonText: z.string(),
|
||||
description: z.string(),
|
||||
gifFile: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
});
|
||||
const CmsSchema = z.object({
|
||||
advantages: z.object({
|
||||
cards: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
description: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
challenges: z.object({
|
||||
cards: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
description: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
chart: z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.number(),
|
||||
}),
|
||||
),
|
||||
missedAmount: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
contact: z.object({
|
||||
description: z.string(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
name: z.string(),
|
||||
placeholder: z.string(),
|
||||
required: z.boolean(),
|
||||
}),
|
||||
),
|
||||
submitButtonText: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
features: z.object({
|
||||
cards: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
),
|
||||
description: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
footer: z.object({
|
||||
copyright: z.string(),
|
||||
email: z.string(),
|
||||
location: z.string(),
|
||||
phone: z.string(),
|
||||
tagline: z.string(),
|
||||
}),
|
||||
hero: z.object({
|
||||
buttonLink: z.string(),
|
||||
buttonText: z.string(),
|
||||
description: z.string(),
|
||||
gifFile: z.string(),
|
||||
title: z.string(),
|
||||
}),
|
||||
created_at: z.number(),
|
||||
updated_at: z.number(),
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
type: z.string(),
|
||||
});
|
||||
export const cmsListResponseSchema = z.object({
|
||||
cms: z.array(CmsSchema),
|
||||
});
|
||||
export const cmsDetailsResponse = z.object({
|
||||
cms: CmsSchema,
|
||||
});
|
||||
export type TCms = z.infer<typeof CmsSchema>;
|
||||
export type TCmsListResponse = z.infer<typeof cmsListResponseSchema>;
|
||||
export type TCmsDetailsResponse = z.infer<typeof cmsDetailsResponse>;
|
||||
export type TCmsCredentials = z.infer<typeof CmsCredentials>;
|
||||
@@ -98,3 +98,15 @@ export function parseWithSchema<T>(
|
||||
throw new Error(`ERROR caught in ${errorContext}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteEmptyKeys = (obj: Record<string, any>) => {
|
||||
const newObj = { ...obj };
|
||||
for (const key in newObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||
if (!newObj[key]) {
|
||||
delete newObj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ export function createTimeFrameExtractor(
|
||||
?.split(",")
|
||||
.find((value) => value.includes(sectionKey));
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user