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:
Sina Nakhostin
2025-11-11 11:55:22 +03:30
63 changed files with 3556 additions and 226 deletions
+1
View File
@@ -0,0 +1 @@
{}
+9 -9
View File
@@ -11,7 +11,7 @@
"@tanstack/react-query": "^5.85.6",
"@types/js-cookie": "^3.0.6",
"@types/react-paginate": "^7.1.4",
"apexcharts": "^4.5.0",
"apexcharts": "^4.7.0",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -26,7 +26,7 @@
"next-themes": "^0.4.4",
"nextjs-toploader": "^3.7.15",
"react": "19.0.0",
"react-apexcharts": "^1.7.0",
"react-apexcharts": "^1.8.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.62.0",
"react-intersection-observer": "^9.16.0",
@@ -2353,9 +2353,9 @@
}
},
"node_modules/apexcharts": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.5.0.tgz",
"integrity": "sha512-E7ZkrVqPNBUWy/Rmg8DEIqHNBmElzICE/oxOX5Ekvs2ICQUOK/VkEkMH09JGJu+O/EA0NL31hxlmF+wrwrSLaQ==",
"version": "4.7.0",
"resolved": "https://registry.npmmirror.com/apexcharts/-/apexcharts-4.7.0.tgz",
"integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
"license": "MIT",
"dependencies": {
"@svgdotjs/svg.draggable.js": "^3.0.4",
@@ -6200,10 +6200,10 @@
}
},
"node_modules/react-apexcharts": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.7.0.tgz",
"integrity": "sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==",
"license": "MIT",
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/react-apexcharts/-/react-apexcharts-1.8.0.tgz",
"integrity": "sha512-MuEp56gc0NMO2UUgY94fxQzoBE4XEjmcCha4xYY0vJdRrc1yfFFZE4QrCekOt2wcS3nibghzca/q/CbgkAgN5w==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"prop-types": "^15.8.1"
},
+2 -2
View File
@@ -12,7 +12,7 @@
"@tanstack/react-query": "^5.85.6",
"@types/js-cookie": "^3.0.6",
"@types/react-paginate": "^7.1.4",
"apexcharts": "^4.5.0",
"apexcharts": "^4.7.0",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -27,7 +27,7 @@
"next-themes": "^0.4.4",
"nextjs-toploader": "^3.7.15",
"react": "19.0.0",
"react-apexcharts": "^1.7.0",
"react-apexcharts": "^1.8.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.62.0",
"react-intersection-observer": "^9.16.0",
+24
View File
@@ -0,0 +1,24 @@
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
import ContactUsTable from "@/components/Tables/contact-us";
import { BreadcrumbItem } from "@/types/shared";
const ContactUsPage = () => {
const breadcrumbItems: BreadcrumbItem[] = [
{
href: "/",
name: "Dashboard",
},
{
href: "/contact-us",
name: "Contact us",
},
];
return (
<>
<Breadcrumb items={breadcrumbItems} />
<ContactUsTable />
</>
);
};
export default ContactUsPage;
+2 -2
View File
@@ -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>
);
}
+143
View File
@@ -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;
+146
View File
@@ -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;
+45
View File
@@ -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;
+56
View File
@@ -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;
+24
View File
@@ -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;
+91
View File
@@ -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
-2
View File
@@ -1,5 +1,3 @@
import Link from "next/link";
import GoogleSigninButton from "../GoogleSigninButton";
import SigninWithPassword from "../SigninWithPassword";
export default function Signin() {
+1 -1
View File
@@ -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",
+60 -8
View File
@@ -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>
+30
View File
@@ -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: [],
},
],
},
];
+26
View File
@@ -19,6 +19,32 @@ export function ChevronUp(props: PropsType) {
</svg>
);
}
export function CallIncome(props: PropsType) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M21.97 18.33C21.97 18.69 21.89 19.06 21.72 19.42C21.55 19.78 21.33 20.12 21.04 20.44C20.55 20.98 20.01 21.37 19.4 21.62C18.8 21.87 18.15 22 17.45 22C16.43 22 15.34 21.76 14.19 21.27C13.04 20.78 11.89 20.12 10.75 19.29C9.6 18.45 8.51 17.52 7.47 16.49C6.44 15.45 5.51 14.36 4.68 13.22C3.86 12.08 3.2 10.94 2.72 9.81C2.24 8.67 2 7.58 2 6.54C2 5.86 2.12 5.21 2.36 4.61C2.6 4 2.98 3.44 3.51 2.94C4.15 2.31 4.85 2 5.59 2C5.87 2 6.15 2.06 6.4 2.18C6.66 2.3 6.89 2.48 7.07 2.74L9.39 6.01C9.57 6.26 9.7 6.49 9.79 6.71C9.88 6.92 9.93 7.13 9.93 7.32C9.93 7.56 9.86 7.8 9.72 8.03C9.59 8.26 9.4 8.5 9.16 8.74L8.4 9.53C8.29 9.64 8.24 9.77 8.24 9.93C8.24 10.01 8.25 10.08 8.27 10.16C8.3 10.24 8.33 10.3 8.35 10.36C8.53 10.69 8.84 11.12 9.28 11.64C9.73 12.16 10.21 12.69 10.73 13.22C11.27 13.75 11.79 14.24 12.32 14.69C12.84 15.13 13.27 15.43 13.61 15.61C13.66 15.63 13.72 15.66 13.79 15.69C13.87 15.72 13.95 15.73 14.04 15.73C14.21 15.73 14.34 15.67 14.45 15.56L15.21 14.81C15.46 14.56 15.7 14.37 15.93 14.25C16.16 14.11 16.39 14.04 16.64 14.04C16.83 14.04 17.03 14.08 17.25 14.17C17.47 14.26 17.7 14.39 17.95 14.56L21.26 16.91C21.52 17.09 21.7 17.3 21.81 17.55C21.91 17.8 21.97 18.05 21.97 18.33Z"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M16.2 7.8H21M16.2 7.8V3V7.8Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function NotificationIcon(props: PropsType) {
return (
<svg
@@ -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;
+50 -18
View File
@@ -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,29 +65,34 @@ const AdminsTable = () => {
</TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody>
{allUsers.map((item, index) => {
{!allUsers.length ? (
<TableRow>
<TableCell>
<h2>No user</h2>
</TableCell>
</TableRow>
) : (
allUsers.map((item, index) => {
return (
<TableRow
key={item.id}
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}
{item?.full_name}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item.email}
{item?.email}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.username}
</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>
<Status status={item?.status ?? ""}>{item?.status}</Status>
</TableCell>
<TableCell className="text-start xl:pr-7.5">
@@ -128,7 +142,8 @@ const AdminsTable = () => {
</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,
};
};
+125
View File
@@ -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;
+2 -4
View File
@@ -21,9 +21,7 @@ import Link from "next/link";
import { Tooltip } from "react-tooltip";
import { useCompanyListPresenter } from "./useCompanyListPresenter";
interface IProps {}
const CompaniesTable = ({}: IProps) => {
const CompaniesTable = () => {
const {
isPending,
pathName,
@@ -47,7 +45,7 @@ const CompaniesTable = ({}: IProps) => {
{columns.map((columns) => (
<TableHead
colSpan={100}
className="text-start font-extrabold uppercase"
className="text-start uppercase"
key={columns}
>
{columns}
@@ -0,0 +1,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,6 +154,7 @@ const CustomersTable = () => {
))}
</TableBody>
</Table>
<div className="w-1/12">
<Modal
onConfirm={() => {
if (!selectedCompanies || !currentCustomer) {
@@ -173,6 +174,7 @@ const CustomersTable = () => {
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,14 +30,24 @@ 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>
<ListHeader
createButtonText="Create notification"
onFilter={() => setIsFilterModalOpen(true)}
/>
<Table>
<TableHeader>
<TableRow>
@@ -66,6 +78,9 @@ const NotificationHistoryTable = () => {
unix: item.created_at,
})}
</TableCell>
<TableCell colSpan={100}>
{item.recipient?.full_name}
</TableCell>
<TableCell colSpan={100} className="truncate">
{truncateString(item.message, 20, 20)}
</TableCell>
@@ -115,6 +130,22 @@ const NotificationHistoryTable = () => {
}}
/>
</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,
};
};
+1
View File
@@ -37,6 +37,7 @@ type ButtonProps = HTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants> & {
label: string;
icon?: React.ReactNode;
type?: "button" | "submit" | "reset";
};
export function Button({
+45
View File
@@ -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;
+154
View File
@@ -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;
+3 -1
View File
@@ -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
+258
View File
@@ -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 dIvoire" },
{ 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 Peoples Republic of)" },
{ value: "KR", label: "Korea (Republic of)" },
{ value: "KW", label: "Kuwait" },
{ value: "KG", label: "Kyrgyzstan" },
{ value: "LA", label: "Lao Peoples 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" },
];
+2 -5
View File
@@ -14,12 +14,9 @@ import React, {
} from "react";
import { useRouter } from "next/navigation";
import { z } from "zod";
import { ILoginResponse, IUser, UserSchema } from "../lib/api/types";
import { COOKIE_KEYS } from "../lib/shared/cookies";
interface UserContextType {
user: IUser | null;
accessToken: string | null;
@@ -27,7 +24,7 @@ interface UserContextType {
isLoading: boolean;
setAuthState: (data: ILoginResponse) => void;
logout: () => void;
setUser:Dispatch<SetStateAction<IUser|null>>
setUser: Dispatch<SetStateAction<IUser | null>>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
@@ -48,7 +45,7 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
router.refresh();
window.location.href = "/auth/sign-in";
}, [router]);
useEffect(() => {
try {
+13 -3
View File
@@ -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";
+11
View File
@@ -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),
});
};
+80
View File
@@ -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)],
});
},
});
};
+13
View File
@@ -0,0 +1,13 @@
import { API_ENDPOINTS } from "@/lib/api";
import { contactUsService } from "@/lib/api/services/contact-us-service";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
export const useReadAllContactUs = () => {
const queryKey = useMemo(() => [API_ENDPOINTS.CONTACT_US.READ_ALL], []);
return useQuery({
queryKey,
queryFn: () => contactUsService.getAllContactUs(),
});
};
@@ -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>) => {
+6
View File
@@ -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;
+53
View File
@@ -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;
}
},
};
+161
View File
@@ -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>;
+12
View File
@@ -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;
};