slider
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
VITE_TOKEN_NAME = 'dmnu_a_t'
|
VITE_TOKEN_NAME = 'dmnu_a_t'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
||||||
|
|
||||||
VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
# VITE_BASE_URL = 'https://dmenu-api.danakcorp.com'
|
||||||
# VITE_BASE_URL = 'http://192.168.99.131:2000'
|
VITE_BASE_URL = 'http://192.168.99.131:2000'
|
||||||
|
|
||||||
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com'
|
||||||
@@ -83,6 +83,11 @@ export const Pages = {
|
|||||||
notifications: {
|
notifications: {
|
||||||
list: "/notifications/list",
|
list: "/notifications/list",
|
||||||
},
|
},
|
||||||
|
sliders: {
|
||||||
|
list: "/sliders/list",
|
||||||
|
add: "/sliders/create",
|
||||||
|
update: "/sliders/update/",
|
||||||
|
},
|
||||||
statistics: {
|
statistics: {
|
||||||
list: "/statistics",
|
list: "/statistics",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const MENU_PREVIEW_RESERVED_SEGMENTS = new Set([
|
|||||||
"reviews",
|
"reviews",
|
||||||
"pagers",
|
"pagers",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"sliders",
|
||||||
"assets",
|
"assets",
|
||||||
"menu-preview",
|
"menu-preview",
|
||||||
]);
|
]);
|
||||||
|
|||||||
+2
-1
@@ -68,7 +68,8 @@
|
|||||||
"reviews": "نظرات",
|
"reviews": "نظرات",
|
||||||
"pagers": "پیجرها",
|
"pagers": "پیجرها",
|
||||||
"notifications": "تنظیمات اعلانات",
|
"notifications": "تنظیمات اعلانات",
|
||||||
"statistics": "گزارشات"
|
"statistics": "گزارشات",
|
||||||
|
"sliders": "اسلایدرها"
|
||||||
},
|
},
|
||||||
"shipment": {
|
"shipment": {
|
||||||
"dineIn": "سرو در رستوران",
|
"dineIn": "سرو در رستوران",
|
||||||
|
|||||||
@@ -208,9 +208,9 @@ export const useCreateOrderForm = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerNewCustomer = async (): Promise<string | null> => {
|
const registerNewCustomer = async (): Promise<string | undefined> => {
|
||||||
if (!isNewCustomer) {
|
if (!isNewCustomer) {
|
||||||
return customerUserId
|
return customerUserId ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await createUser({
|
const response = await createUser({
|
||||||
|
|||||||
@@ -26,4 +26,5 @@ export const enum PermissionEnum {
|
|||||||
MANAGE_SCHEDULES = "manage_schedules",
|
MANAGE_SCHEDULES = "manage_schedules",
|
||||||
MANAGE_REPORTS = "manage_reports",
|
MANAGE_REPORTS = "manage_reports",
|
||||||
MANAGE_CONTACTS = "manage_contacts",
|
MANAGE_CONTACTS = "manage_contacts",
|
||||||
|
MANAGE_SLIDERS = "manage_sliders",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { type FC, useState } from "react";
|
||||||
|
import { useFormik } from "formik";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { TickCircle } from "iconsax-react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Button from "@/components/Button";
|
||||||
|
import Input from "@/components/Input";
|
||||||
|
import { Pages } from "@/config/Pages";
|
||||||
|
import type { ErrorType } from "@/helpers/types";
|
||||||
|
import { extractErrorMessage } from "@/config/func";
|
||||||
|
import { useCreateSlider } from "./hooks/useSliderData";
|
||||||
|
import { useSingleUpload } from "../uploader/hooks/useUploaderData";
|
||||||
|
import type { CreateSliderType } from "./types/Types";
|
||||||
|
import SliderSidebar from "./components/SliderSidebar";
|
||||||
|
|
||||||
|
const CreateSlider: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutate: createSlider, isPending } = useCreateSlider();
|
||||||
|
const { mutate: singleUpload, isPending: isUploading } = useSingleUpload();
|
||||||
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
|
const formik = useFormik<CreateSliderType>({
|
||||||
|
initialValues: {
|
||||||
|
imageUrl: "",
|
||||||
|
title: "",
|
||||||
|
link: "",
|
||||||
|
order: 0,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
title: Yup.string().required("عنوان الزامی است"),
|
||||||
|
link: Yup.string()
|
||||||
|
.transform((value) => (value?.trim() === "" ? undefined : value))
|
||||||
|
.url("لینک معتبر نیست")
|
||||||
|
.notRequired(),
|
||||||
|
order: Yup.number().required("ترتیب الزامی است").min(0, "ترتیب باید مثبت باشد"),
|
||||||
|
isActive: Yup.boolean(),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
if (imageFiles.length === 0) {
|
||||||
|
toast.error("تصویر اسلایدر الزامی است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitSlider = (imageUrl: string) => {
|
||||||
|
createSlider(
|
||||||
|
{
|
||||||
|
...values,
|
||||||
|
imageUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("اسلایدر با موفقیت ایجاد شد");
|
||||||
|
navigate(Pages.sliders.list);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
singleUpload(imageFiles[0], {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const uploadedImageUrl = response?.data?.url || "";
|
||||||
|
if (!uploadedImageUrl) {
|
||||||
|
toast.error("آدرس تصویر آپلود شده دریافت نشد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitSlider(uploadedImageUrl);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error, "خطا در آپلود تصویر"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="flex flex-col sm:flex-row w-full justify-between items-start sm:items-center gap-4">
|
||||||
|
<div className="text-lg font-light">افزودن اسلایدر</div>
|
||||||
|
<Button
|
||||||
|
className="px-5 w-full sm:w-auto"
|
||||||
|
onClick={async () => {
|
||||||
|
const errors = await formik.validateForm();
|
||||||
|
formik.setTouched({
|
||||||
|
title: true,
|
||||||
|
link: true,
|
||||||
|
order: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
const firstError = Object.values(errors).find((error) => typeof error === "string");
|
||||||
|
if (firstError) toast.error(firstError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFiles.length === 0) {
|
||||||
|
toast.error("تصویر اسلایدر الزامی است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.handleSubmit();
|
||||||
|
}}
|
||||||
|
isloading={isPending || isUploading}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<TickCircle className="size-5" color="white" />
|
||||||
|
<div>ثبت اسلایدر</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6 mt-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
|
||||||
|
<Input
|
||||||
|
label="عنوان"
|
||||||
|
name="title"
|
||||||
|
value={formik.values.title}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ""}
|
||||||
|
/>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Input
|
||||||
|
label="لینک"
|
||||||
|
name="link"
|
||||||
|
value={formik.values.link}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Input
|
||||||
|
label="ترتیب"
|
||||||
|
name="order"
|
||||||
|
type="number"
|
||||||
|
value={formik.values.order ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value === "" ? 0 : Number(e.target.value);
|
||||||
|
formik.setFieldValue("order", value);
|
||||||
|
}}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full lg:w-auto">
|
||||||
|
<SliderSidebar
|
||||||
|
isActive={formik.values.isActive}
|
||||||
|
onChangeActive={(value) => formik.setFieldValue("isActive", value)}
|
||||||
|
onChangeImage={setImageFiles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateSlider;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { type FC } from "react";
|
||||||
|
import Table from "@/components/Table";
|
||||||
|
import Filters from "@/components/Filters";
|
||||||
|
import { Add } from "iconsax-react";
|
||||||
|
import Button from "@/components/Button";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Pages } from "@/config/Pages";
|
||||||
|
import { useGetSliders, useDeleteSlider } from "./hooks/useSliderData";
|
||||||
|
import { useSliderFilters } from "./hooks/useSliderFilters";
|
||||||
|
import { getSliderTableColumns } from "./components/SliderTableColumns";
|
||||||
|
import { useSliderFiltersFields } from "./components/SliderFiltersFields";
|
||||||
|
|
||||||
|
const SlidersList: FC = () => {
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
currentPage,
|
||||||
|
apiParams,
|
||||||
|
handleFiltersChange,
|
||||||
|
handlePageChange,
|
||||||
|
limit,
|
||||||
|
} = useSliderFilters();
|
||||||
|
|
||||||
|
const { data: slidersData, isLoading } = useGetSliders(apiParams);
|
||||||
|
const { mutate: deleteSlider, isPending: isDeleting } = useDeleteSlider();
|
||||||
|
|
||||||
|
const sliders = slidersData?.data || [];
|
||||||
|
const columns = getSliderTableColumns({
|
||||||
|
onDelete: (id: string) => deleteSlider(id),
|
||||||
|
isDeleting,
|
||||||
|
});
|
||||||
|
const filterFields = useSliderFiltersFields();
|
||||||
|
const totalPages = Math.ceil(sliders.length / limit) || 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-lg font-light">لیست اسلایدرها</h1>
|
||||||
|
<Link to={Pages.sliders.add}>
|
||||||
|
<Button className="w-fit px-6">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Add color="#fff" size={20} />
|
||||||
|
<span>افزودن اسلایدر</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Filters
|
||||||
|
fields={filterFields}
|
||||||
|
onChange={handleFiltersChange}
|
||||||
|
initialValues={filters}
|
||||||
|
searchField="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={sliders}
|
||||||
|
isloading={isLoading}
|
||||||
|
pagination={{
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange: handlePageChange,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SlidersList;
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { type FC, useMemo, useState } from "react";
|
||||||
|
import { useFormik } from "formik";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { TickCircle } from "iconsax-react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import Button from "@/components/Button";
|
||||||
|
import Input from "@/components/Input";
|
||||||
|
import { Pages } from "@/config/Pages";
|
||||||
|
import type { ErrorType } from "@/helpers/types";
|
||||||
|
import { extractErrorMessage } from "@/config/func";
|
||||||
|
import { useGetSlider, useUpdateSlider } from "./hooks/useSliderData";
|
||||||
|
import { useSingleUpload } from "../uploader/hooks/useUploaderData";
|
||||||
|
import type { CreateSliderType } from "./types/Types";
|
||||||
|
import SliderSidebar from "./components/SliderSidebar";
|
||||||
|
|
||||||
|
const UpdateSlider: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const { mutate: updateSlider, isPending } = useUpdateSlider();
|
||||||
|
const { mutate: singleUpload, isPending: isUploading } = useSingleUpload();
|
||||||
|
const { data: sliderData, isLoading } = useGetSlider(id || "");
|
||||||
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
|
const initialValues = useMemo<CreateSliderType>(() => {
|
||||||
|
if (sliderData?.data) {
|
||||||
|
return {
|
||||||
|
imageUrl: sliderData.data.imageUrl || "",
|
||||||
|
title: sliderData.data.title || "",
|
||||||
|
link: sliderData.data.link || "",
|
||||||
|
order: sliderData.data.order ?? 0,
|
||||||
|
isActive: sliderData.data.isActive ?? true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageUrl: "",
|
||||||
|
title: "",
|
||||||
|
link: "",
|
||||||
|
order: 0,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
}, [sliderData]);
|
||||||
|
|
||||||
|
const formik = useFormik<CreateSliderType>({
|
||||||
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
validationSchema: Yup.object().shape({
|
||||||
|
title: Yup.string().required("عنوان الزامی است"),
|
||||||
|
link: Yup.string()
|
||||||
|
.transform((value) => (value?.trim() === "" ? undefined : value))
|
||||||
|
.url("لینک معتبر نیست")
|
||||||
|
.notRequired(),
|
||||||
|
order: Yup.number().required("ترتیب الزامی است").min(0, "ترتیب باید مثبت باشد"),
|
||||||
|
isActive: Yup.boolean(),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
if (imageFiles.length === 0 && !values.imageUrl) {
|
||||||
|
toast.error("تصویر اسلایدر الزامی است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitSlider = (imageUrl: string) => {
|
||||||
|
updateSlider(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
params: {
|
||||||
|
...values,
|
||||||
|
imageUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("اسلایدر با موفقیت ویرایش شد");
|
||||||
|
navigate(Pages.sliders.list);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
singleUpload(imageFiles[0], {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
const uploadedImageUrl = response?.data?.url || "";
|
||||||
|
if (!uploadedImageUrl) {
|
||||||
|
toast.error("آدرس تصویر آپلود شده دریافت نشد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitSlider(uploadedImageUrl);
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(extractErrorMessage(error, "خطا در آپلود تصویر"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitSlider(values.imageUrl);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="text-lg font-light">در حال بارگذاری...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="flex flex-col sm:flex-row w-full justify-between items-start sm:items-center gap-4">
|
||||||
|
<div className="text-lg font-light">ویرایش اسلایدر</div>
|
||||||
|
<Button
|
||||||
|
className="px-5 w-full sm:w-auto"
|
||||||
|
onClick={async () => {
|
||||||
|
const errors = await formik.validateForm();
|
||||||
|
formik.setTouched({
|
||||||
|
title: true,
|
||||||
|
link: true,
|
||||||
|
order: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
const firstError = Object.values(errors).find((error) => typeof error === "string");
|
||||||
|
if (firstError) toast.error(firstError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFiles.length === 0 && !formik.values.imageUrl) {
|
||||||
|
toast.error("تصویر اسلایدر الزامی است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.handleSubmit();
|
||||||
|
}}
|
||||||
|
isloading={isPending || isUploading}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<TickCircle className="size-5" color="white" />
|
||||||
|
<div>ثبت تغییرات</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6 mt-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="bg-white py-6 sm:py-8 xl:px-10 px-4 rounded-3xl">
|
||||||
|
<Input
|
||||||
|
label="عنوان"
|
||||||
|
name="title"
|
||||||
|
value={formik.values.title}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ""}
|
||||||
|
/>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Input
|
||||||
|
label="لینک"
|
||||||
|
name="link"
|
||||||
|
value={formik.values.link}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.link && formik.errors.link ? formik.errors.link : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Input
|
||||||
|
label="ترتیب"
|
||||||
|
name="order"
|
||||||
|
type="number"
|
||||||
|
value={formik.values.order ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value === "" ? 0 : Number(e.target.value);
|
||||||
|
formik.setFieldValue("order", value);
|
||||||
|
}}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full lg:w-auto">
|
||||||
|
<SliderSidebar
|
||||||
|
isActive={formik.values.isActive}
|
||||||
|
onChangeActive={(value) => formik.setFieldValue("isActive", value)}
|
||||||
|
onChangeImage={setImageFiles}
|
||||||
|
preview={formik.values.imageUrl ? [formik.values.imageUrl] : []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateSlider;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import type { FieldType } from "@/components/Filters";
|
||||||
|
|
||||||
|
export const useSliderFiltersFields = (): FieldType[] => {
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "search",
|
||||||
|
placeholder: "جستجو",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { type FC } from "react";
|
||||||
|
import UploadBoxDraggble from "@/components/UploadBoxDraggble";
|
||||||
|
import Switch from "@/components/Switch";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isActive: boolean;
|
||||||
|
onChangeActive: (value: boolean) => void;
|
||||||
|
onChangeImage: (files: File[]) => void;
|
||||||
|
preview?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SliderSidebar: FC<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white w-full lg:w-[300px] text-xs h-fit px-5 py-7 rounded-3xl">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>وضعیت اسلایدر</div>
|
||||||
|
<Switch
|
||||||
|
active={props.isActive}
|
||||||
|
onChange={(value) => props.onChangeActive(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className="text-sm mb-2">تصویر اسلایدر</div>
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label="تصویر اسلایدر را آپلود کنید"
|
||||||
|
onChange={(files) => props.onChangeImage(files)}
|
||||||
|
preview={props.preview}
|
||||||
|
imageSize="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SliderSidebar;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { ColumnType } from "@/components/types/TableTypes";
|
||||||
|
import type { Slider } from "../types/Types";
|
||||||
|
import TrashWithConfrim from "@/components/TrashWithConfrim";
|
||||||
|
import { Eye } from "iconsax-react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Pages } from "@/config/Pages";
|
||||||
|
import Status from "@/components/Status";
|
||||||
|
|
||||||
|
interface GetSliderTableColumnsParams {
|
||||||
|
onDelete?: (id: string) => void;
|
||||||
|
isDeleting?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSliderTableColumns = ({
|
||||||
|
onDelete,
|
||||||
|
isDeleting,
|
||||||
|
}: GetSliderTableColumnsParams = {}): ColumnType<Slider>[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "title",
|
||||||
|
title: "عنوان",
|
||||||
|
render: (item: Slider) => item.title || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "imageUrl",
|
||||||
|
title: "تصویر",
|
||||||
|
render: (item: Slider) => (
|
||||||
|
<img
|
||||||
|
src={item.imageUrl}
|
||||||
|
alt={item.title}
|
||||||
|
className="w-16 h-10 rounded object-cover"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "link",
|
||||||
|
title: "لینک",
|
||||||
|
render: (item: Slider) => item.link || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "order",
|
||||||
|
title: "ترتیب",
|
||||||
|
render: (item: Slider) => item.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "isActive",
|
||||||
|
title: "وضعیت",
|
||||||
|
render: (item: Slider) => (
|
||||||
|
<Status
|
||||||
|
variant={item.isActive ? "success" : "error"}
|
||||||
|
label={item.isActive ? "فعال" : "غیرفعال"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
title: "",
|
||||||
|
render: (item: Slider) => (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Link to={Pages.sliders.update + item.id}>
|
||||||
|
<Eye size={20} color="#8C90A3" className="cursor-pointer" />
|
||||||
|
</Link>
|
||||||
|
{onDelete && (
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => onDelete(item.id)}
|
||||||
|
isloading={isDeleting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/SliderService";
|
||||||
|
import type { CreateSliderType, GetSlidersParams } from "../types/Types";
|
||||||
|
|
||||||
|
export const useGetSliders = (params?: GetSlidersParams) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["sliders", params],
|
||||||
|
queryFn: () => api.getSliders(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSlider = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createSlider,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["sliders"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSlider = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteSlider,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["sliders"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSlider = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["slider", id],
|
||||||
|
queryFn: () => api.getSlider(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSlider = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, params }: { id: string; params: CreateSliderType }) =>
|
||||||
|
api.updateSlider(id, params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["sliders"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import type { FilterValues } from "@/components/Filters";
|
||||||
|
import type { GetSlidersParams } from "../types/Types";
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = 1;
|
||||||
|
const DEFAULT_LIMIT = 10;
|
||||||
|
|
||||||
|
export const useSliderFilters = () => {
|
||||||
|
const [filters, setFilters] = useState<FilterValues>({});
|
||||||
|
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
|
||||||
|
const [limit] = useState(DEFAULT_LIMIT);
|
||||||
|
|
||||||
|
const apiParams = useMemo<GetSlidersParams>(() => {
|
||||||
|
const params: GetSlidersParams = {
|
||||||
|
page: currentPage,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
params.search = filters.search as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}, [filters, currentPage, limit]);
|
||||||
|
|
||||||
|
const handleFiltersChange = (newFilters: FilterValues) => {
|
||||||
|
setFilters(newFilters);
|
||||||
|
setCurrentPage(DEFAULT_PAGE);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
currentPage,
|
||||||
|
apiParams,
|
||||||
|
handleFiltersChange,
|
||||||
|
handlePageChange,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import type {
|
||||||
|
CreateSliderType,
|
||||||
|
GetSlidersParams,
|
||||||
|
SliderResponse,
|
||||||
|
SlidersResponse,
|
||||||
|
} from "../types/Types";
|
||||||
|
|
||||||
|
export const getSliders = async (
|
||||||
|
params?: GetSlidersParams
|
||||||
|
): Promise<SlidersResponse> => {
|
||||||
|
const { data } = await axios.get<SlidersResponse>("/admin/sliders", { params });
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSlider = async (params: CreateSliderType) => {
|
||||||
|
const { data } = await axios.post("/admin/sliders", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSlider = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/sliders/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSlider = async (id: string): Promise<SliderResponse> => {
|
||||||
|
const { data } = await axios.get<SliderResponse>(`/admin/sliders/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSlider = async (id: string, params: CreateSliderType) => {
|
||||||
|
const { data } = await axios.patch(`/admin/sliders/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { IResponse } from "@/types/response.types";
|
||||||
|
|
||||||
|
export type Slider = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
imageUrl: string;
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
order: number;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SlidersResponse = IResponse<Slider[]>;
|
||||||
|
export type SliderResponse = IResponse<Slider>;
|
||||||
|
|
||||||
|
export type CreateSliderType = {
|
||||||
|
imageUrl: string;
|
||||||
|
title: string;
|
||||||
|
link?: string;
|
||||||
|
order: number;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSlidersParams = {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
@@ -47,6 +47,9 @@ import NotificationsList from '@/pages/notifications/List'
|
|||||||
import Notification from '@/components/Notification'
|
import Notification from '@/components/Notification'
|
||||||
import ReportsList from '@/pages/report/List'
|
import ReportsList from '@/pages/report/List'
|
||||||
import Statistics from '@/pages/statistics/Statistics'
|
import Statistics from '@/pages/statistics/Statistics'
|
||||||
|
import SlidersList from '@/pages/sliders/List'
|
||||||
|
import CreateSlider from '@/pages/sliders/Create'
|
||||||
|
import UpdateSlider from '@/pages/sliders/Update'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -112,6 +115,9 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.pagers.list} element={<PagersList />} />
|
<Route path={Pages.pagers.list} element={<PagersList />} />
|
||||||
|
|
||||||
<Route path={Pages.notifications.list} element={<NotificationsList />} />
|
<Route path={Pages.notifications.list} element={<NotificationsList />} />
|
||||||
|
<Route path={Pages.sliders.list} element={<SlidersList />} />
|
||||||
|
<Route path={Pages.sliders.add} element={<CreateSlider />} />
|
||||||
|
<Route path={Pages.sliders.update + ':id'} element={<UpdateSlider />} />
|
||||||
|
|
||||||
<Route path={Pages.reports.list} element={<ReportsList />} />
|
<Route path={Pages.reports.list} element={<ReportsList />} />
|
||||||
|
|
||||||
|
|||||||
+24
-15
@@ -1,21 +1,21 @@
|
|||||||
import { type FC, useEffect } from 'react'
|
|
||||||
import LogoImage from '../assets/images/logo.png'
|
|
||||||
import LogoSmall from '../assets/images/logo-small.png'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { DocumentText, Home2, Logout, People, Setting2, TicketDiscount, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing, SmsEdit } from 'iconsax-react'
|
|
||||||
import SideBarItem from './SideBarItem'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import { Pages } from '../config/Pages'
|
|
||||||
import { useSharedStore } from './store/sharedStore'
|
|
||||||
import { clx } from '../helpers/utils'
|
|
||||||
import FoodsSubMenu from './components/FoodsSubMenu'
|
|
||||||
import OrdersSubMenu from './components/OrdersSubMenu'
|
|
||||||
import CustomersSubMenu from './components/CustomersSubMenu'
|
|
||||||
import DiscountSubMenu from './components/DiscountSubMenu'
|
|
||||||
import { SideBarItemHasSubMenu } from '../config/SideBarSubMenu'
|
|
||||||
import { usePermissions } from '@/hooks/usePermissions'
|
import { usePermissions } from '@/hooks/usePermissions'
|
||||||
import { PermissionEnum } from '@/pages/roles/enum/Enum'
|
import { PermissionEnum } from '@/pages/roles/enum/Enum'
|
||||||
|
import { Calendar, Card, DocumentText, Element3, Gallery, Home2, Logout, NotificationBing, People, Security, SecurityUser, Setting2, SmsEdit, Star1, TicketDiscount, TruckFast } from 'iconsax-react'
|
||||||
|
import { type FC, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Skeleton from 'react-loading-skeleton'
|
import Skeleton from 'react-loading-skeleton'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
import LogoSmall from '../assets/images/logo-small.png'
|
||||||
|
import LogoImage from '../assets/images/logo.png'
|
||||||
|
import { Pages } from '../config/Pages'
|
||||||
|
import { SideBarItemHasSubMenu } from '../config/SideBarSubMenu'
|
||||||
|
import { clx } from '../helpers/utils'
|
||||||
|
import CustomersSubMenu from './components/CustomersSubMenu'
|
||||||
|
import DiscountSubMenu from './components/DiscountSubMenu'
|
||||||
|
import FoodsSubMenu from './components/FoodsSubMenu'
|
||||||
|
import OrdersSubMenu from './components/OrdersSubMenu'
|
||||||
|
import SideBarItem from './SideBarItem'
|
||||||
|
import { useSharedStore } from './store/sharedStore'
|
||||||
|
|
||||||
const SideBar: FC = () => {
|
const SideBar: FC = () => {
|
||||||
|
|
||||||
@@ -289,6 +289,15 @@ const SideBar: FC = () => {
|
|||||||
activeName='reports'
|
activeName='reports'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{checkPermission(PermissionEnum.MANAGE_SLIDERS) && (
|
||||||
|
<SideBarItem
|
||||||
|
icon={<Gallery variant={isActive('sliders') ? 'Bold' : 'Outline'} color={isActive('sliders') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
|
title={t('sidebar.sliders')}
|
||||||
|
isActive={isActive('sliders')}
|
||||||
|
link={Pages.sliders.list}
|
||||||
|
activeName='sliders'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user