From eb9973678c10c441977f95d3decb7b381872a94d Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 4 Jul 2026 09:45:24 +0330 Subject: [PATCH] slider --- .env | 4 +- src/config/Pages.ts | 5 + src/config/menuPreview.ts | 1 + src/langs/fa.json | 3 +- src/pages/orders/hooks/useCreateOrderForm.ts | 4 +- src/pages/roles/enum/Enum.ts | 1 + src/pages/sliders/Create.tsx | 163 ++++++++++++++ src/pages/sliders/List.tsx | 71 +++++++ src/pages/sliders/Update.tsx | 199 ++++++++++++++++++ .../components/SliderFiltersFields.tsx | 15 ++ .../sliders/components/SliderSidebar.tsx | 36 ++++ .../sliders/components/SliderTableColumns.tsx | 73 +++++++ src/pages/sliders/hooks/useSliderData.ts | 55 +++++ src/pages/sliders/hooks/useSliderFilters.ts | 43 ++++ src/pages/sliders/service/SliderService.ts | 34 +++ src/pages/sliders/types/Types.ts | 30 +++ src/router/Main.tsx | 6 + src/shared/SideBar.tsx | 39 ++-- 18 files changed, 762 insertions(+), 20 deletions(-) create mode 100644 src/pages/sliders/Create.tsx create mode 100644 src/pages/sliders/List.tsx create mode 100644 src/pages/sliders/Update.tsx create mode 100644 src/pages/sliders/components/SliderFiltersFields.tsx create mode 100644 src/pages/sliders/components/SliderSidebar.tsx create mode 100644 src/pages/sliders/components/SliderTableColumns.tsx create mode 100644 src/pages/sliders/hooks/useSliderData.ts create mode 100644 src/pages/sliders/hooks/useSliderFilters.ts create mode 100644 src/pages/sliders/service/SliderService.ts create mode 100644 src/pages/sliders/types/Types.ts diff --git a/.env b/.env index 7b5e9c6..d0609b0 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ VITE_TOKEN_NAME = 'dmnu_a_t' VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt' -VITE_BASE_URL = 'https://dmenu-api.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.99.131:2000' +# VITE_BASE_URL = 'https://dmenu-api.danakcorp.com' +VITE_BASE_URL = 'http://192.168.99.131:2000' VITE_SOCKET_URL = 'https://dmenu-api.danakcorp.com' \ No newline at end of file diff --git a/src/config/Pages.ts b/src/config/Pages.ts index ba2b4dd..b1c4ef1 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -83,6 +83,11 @@ export const Pages = { notifications: { list: "/notifications/list", }, + sliders: { + list: "/sliders/list", + add: "/sliders/create", + update: "/sliders/update/", + }, statistics: { list: "/statistics", }, diff --git a/src/config/menuPreview.ts b/src/config/menuPreview.ts index 9e9dc23..dfd4c84 100644 --- a/src/config/menuPreview.ts +++ b/src/config/menuPreview.ts @@ -22,6 +22,7 @@ export const MENU_PREVIEW_RESERVED_SEGMENTS = new Set([ "reviews", "pagers", "notifications", + "sliders", "assets", "menu-preview", ]); diff --git a/src/langs/fa.json b/src/langs/fa.json index 18eed5a..d9a4c34 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -68,7 +68,8 @@ "reviews": "نظرات", "pagers": "پیجرها", "notifications": "تنظیمات اعلانات", - "statistics": "گزارشات" + "statistics": "گزارشات", + "sliders": "اسلایدرها" }, "shipment": { "dineIn": "سرو در رستوران", diff --git a/src/pages/orders/hooks/useCreateOrderForm.ts b/src/pages/orders/hooks/useCreateOrderForm.ts index 921677f..e921d37 100644 --- a/src/pages/orders/hooks/useCreateOrderForm.ts +++ b/src/pages/orders/hooks/useCreateOrderForm.ts @@ -208,9 +208,9 @@ export const useCreateOrderForm = () => { }) } - const registerNewCustomer = async (): Promise => { + const registerNewCustomer = async (): Promise => { if (!isNewCustomer) { - return customerUserId + return customerUserId ?? undefined } const response = await createUser({ diff --git a/src/pages/roles/enum/Enum.ts b/src/pages/roles/enum/Enum.ts index d9f0073..7139e80 100644 --- a/src/pages/roles/enum/Enum.ts +++ b/src/pages/roles/enum/Enum.ts @@ -26,4 +26,5 @@ export const enum PermissionEnum { MANAGE_SCHEDULES = "manage_schedules", MANAGE_REPORTS = "manage_reports", MANAGE_CONTACTS = "manage_contacts", + MANAGE_SLIDERS = "manage_sliders", } diff --git a/src/pages/sliders/Create.tsx b/src/pages/sliders/Create.tsx new file mode 100644 index 0000000..674154f --- /dev/null +++ b/src/pages/sliders/Create.tsx @@ -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([]); + + const formik = useFormik({ + 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 ( +
+
+
افزودن اسلایدر
+ +
+ +
+
+
+ +
+ +
+
+ { + 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 : ""} + /> +
+
+
+ +
+ formik.setFieldValue("isActive", value)} + onChangeImage={setImageFiles} + /> +
+
+
+ ); +}; + +export default CreateSlider; diff --git a/src/pages/sliders/List.tsx b/src/pages/sliders/List.tsx new file mode 100644 index 0000000..3c3a0b6 --- /dev/null +++ b/src/pages/sliders/List.tsx @@ -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 ( +
+
+

لیست اسلایدرها

+ + + +
+ +
+ +
+ + + + ); +}; + +export default SlidersList; diff --git a/src/pages/sliders/Update.tsx b/src/pages/sliders/Update.tsx new file mode 100644 index 0000000..cd924c6 --- /dev/null +++ b/src/pages/sliders/Update.tsx @@ -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([]); + + const initialValues = useMemo(() => { + 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({ + 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 ( +
+
در حال بارگذاری...
+
+ ); + } + + return ( +
+
+
ویرایش اسلایدر
+ +
+ +
+
+
+ +
+ +
+
+ { + 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 : ""} + /> +
+
+
+ +
+ formik.setFieldValue("isActive", value)} + onChangeImage={setImageFiles} + preview={formik.values.imageUrl ? [formik.values.imageUrl] : []} + /> +
+
+
+ ); +}; + +export default UpdateSlider; diff --git a/src/pages/sliders/components/SliderFiltersFields.tsx b/src/pages/sliders/components/SliderFiltersFields.tsx new file mode 100644 index 0000000..fcd38ca --- /dev/null +++ b/src/pages/sliders/components/SliderFiltersFields.tsx @@ -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: "جستجو", + }, + ], + [] + ); +}; diff --git a/src/pages/sliders/components/SliderSidebar.tsx b/src/pages/sliders/components/SliderSidebar.tsx new file mode 100644 index 0000000..c3bee2e --- /dev/null +++ b/src/pages/sliders/components/SliderSidebar.tsx @@ -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) => { + return ( +
+
+
وضعیت اسلایدر
+ props.onChangeActive(value)} + /> +
+ +
+
تصویر اسلایدر
+ props.onChangeImage(files)} + preview={props.preview} + imageSize="large" + /> +
+
+ ); +}; + +export default SliderSidebar; diff --git a/src/pages/sliders/components/SliderTableColumns.tsx b/src/pages/sliders/components/SliderTableColumns.tsx new file mode 100644 index 0000000..bb7f7f7 --- /dev/null +++ b/src/pages/sliders/components/SliderTableColumns.tsx @@ -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[] => { + return [ + { + key: "title", + title: "عنوان", + render: (item: Slider) => item.title || "-", + }, + { + key: "imageUrl", + title: "تصویر", + render: (item: Slider) => ( + {item.title} + ), + }, + { + key: "link", + title: "لینک", + render: (item: Slider) => item.link || "-", + }, + { + key: "order", + title: "ترتیب", + render: (item: Slider) => item.order, + }, + { + key: "isActive", + title: "وضعیت", + render: (item: Slider) => ( + + ), + }, + { + key: "actions", + title: "", + render: (item: Slider) => ( +
+ + + + {onDelete && ( + onDelete(item.id)} + isloading={isDeleting} + /> + )} +
+ ), + }, + ]; +}; diff --git a/src/pages/sliders/hooks/useSliderData.ts b/src/pages/sliders/hooks/useSliderData.ts new file mode 100644 index 0000000..6b0ddbc --- /dev/null +++ b/src/pages/sliders/hooks/useSliderData.ts @@ -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"], + }); + }, + }); +}; diff --git a/src/pages/sliders/hooks/useSliderFilters.ts b/src/pages/sliders/hooks/useSliderFilters.ts new file mode 100644 index 0000000..78876c9 --- /dev/null +++ b/src/pages/sliders/hooks/useSliderFilters.ts @@ -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({}); + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE); + const [limit] = useState(DEFAULT_LIMIT); + + const apiParams = useMemo(() => { + 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, + }; +}; diff --git a/src/pages/sliders/service/SliderService.ts b/src/pages/sliders/service/SliderService.ts new file mode 100644 index 0000000..33d2f90 --- /dev/null +++ b/src/pages/sliders/service/SliderService.ts @@ -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 => { + const { data } = await axios.get("/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 => { + const { data } = await axios.get(`/admin/sliders/${id}`); + return data; +}; + +export const updateSlider = async (id: string, params: CreateSliderType) => { + const { data } = await axios.patch(`/admin/sliders/${id}`, params); + return data; +}; diff --git a/src/pages/sliders/types/Types.ts b/src/pages/sliders/types/Types.ts new file mode 100644 index 0000000..ebba665 --- /dev/null +++ b/src/pages/sliders/types/Types.ts @@ -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; +export type SliderResponse = IResponse; + +export type CreateSliderType = { + imageUrl: string; + title: string; + link?: string; + order: number; + isActive: boolean; +}; + +export type GetSlidersParams = { + page?: number; + limit?: number; + search?: string; +}; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 7b3a923..ce23fca 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -47,6 +47,9 @@ import NotificationsList from '@/pages/notifications/List' import Notification from '@/components/Notification' import ReportsList from '@/pages/report/List' 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 { hasSubMenu } = useSharedStore() @@ -112,6 +115,9 @@ const MainRouter: FC = () => { } /> } /> + } /> + } /> + } /> } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index b290d29..a22a9c0 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -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 { 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 { 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 = () => { @@ -289,6 +289,15 @@ const SideBar: FC = () => { activeName='reports' /> )} + {checkPermission(PermissionEnum.MANAGE_SLIDERS) && ( + } + title={t('sidebar.sliders')} + isActive={isActive('sliders')} + link={Pages.sliders.list} + activeName='sliders' + /> + )} )}