crud slider + fix footer
This commit is contained in:
@@ -72,6 +72,7 @@ export const Pages = {
|
|||||||
setting: {
|
setting: {
|
||||||
shop: "/settings/shop",
|
shop: "/settings/shop",
|
||||||
banners: "/settings/banners",
|
banners: "/settings/banners",
|
||||||
|
sliders: "/settings/sliders",
|
||||||
siteSetting: "/settings/site-setting",
|
siteSetting: "/settings/site-setting",
|
||||||
},
|
},
|
||||||
wallet: "/wallet",
|
wallet: "/wallet",
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ interface AttributeSelectorProps {
|
|||||||
onChange: (attributeId: number, values: number[]) => void;
|
onChange: (attributeId: number, values: number[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TextNumberInputProps {
|
||||||
|
attribute: CategoryAttributeType;
|
||||||
|
value: string;
|
||||||
|
onChange: (attributeId: number, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const AttributeSelector: FC<AttributeSelectorProps> = ({ attribute, selectedValues, onChange }) => {
|
const AttributeSelector: FC<AttributeSelectorProps> = ({ attribute, selectedValues, onChange }) => {
|
||||||
const handleValueChange = (valueId: number, checked: boolean) => {
|
const handleValueChange = (valueId: number, checked: boolean) => {
|
||||||
if (attribute.multiple) {
|
if (attribute.multiple) {
|
||||||
@@ -61,6 +67,55 @@ const AttributeSelector: FC<AttributeSelectorProps> = ({ attribute, selectedValu
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TextNumberInput: FC<TextNumberInputProps> = ({ attribute, value, onChange }) => {
|
||||||
|
if (attribute.type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<label className="block text-sm font-semibold text-gray-800 mb-2">
|
||||||
|
{attribute.title}
|
||||||
|
{attribute.required && <span className="text-red-500 mr-1">*</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{attribute.hint && (
|
||||||
|
<p className="text-xs text-gray-500 mb-3">{attribute.hint}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value === 'true'}
|
||||||
|
onChange={(e) => onChange(attribute._id, e.target.checked ? 'true' : 'false')}
|
||||||
|
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">بله</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<label className="block text-sm font-semibold text-gray-800 mb-2">
|
||||||
|
{attribute.title}
|
||||||
|
{attribute.required && <span className="text-red-500 mr-1">*</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{attribute.hint && (
|
||||||
|
<p className="text-xs text-gray-500 mb-3">{attribute.hint}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type={attribute.type === 'number' ? 'number' : 'text'}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(attribute._id, e.target.value)}
|
||||||
|
placeholder={`مقدار ${attribute.title} را وارد کنید`}
|
||||||
|
className="w-full"
|
||||||
|
required={attribute.required}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface Step2FormProps {
|
interface Step2FormProps {
|
||||||
onSubmit: (data: Omit<CreateProductAttributeRequestType, 'productId'>) => void;
|
onSubmit: (data: Omit<CreateProductAttributeRequestType, 'productId'>) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -69,6 +124,11 @@ interface Step2FormProps {
|
|||||||
initialData?: Partial<Omit<CreateProductAttributeRequestType, 'productId'>>;
|
initialData?: Partial<Omit<CreateProductAttributeRequestType, 'productId'>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AttributeWithValue {
|
||||||
|
id: number;
|
||||||
|
values: number[] | string[];
|
||||||
|
}
|
||||||
|
|
||||||
// کامپوننت مدیریت تگها
|
// کامپوننت مدیریت تگها
|
||||||
interface TagInputProps {
|
interface TagInputProps {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
@@ -200,7 +260,7 @@ const ListInput: FC<ListInputProps> = ({ items, onChange, placeholder, label })
|
|||||||
|
|
||||||
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId, initialData }) => {
|
const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, categoryId, initialData }) => {
|
||||||
const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>(() => ({
|
const [formData, setFormData] = useState<Omit<CreateProductAttributeRequestType, 'productId'>>(() => ({
|
||||||
attributes: initialData?.attributes || [],
|
attributes: (initialData?.attributes || []) as AttributeWithValue[],
|
||||||
description: initialData?.description || '',
|
description: initialData?.description || '',
|
||||||
metaDescription: initialData?.metaDescription || '',
|
metaDescription: initialData?.metaDescription || '',
|
||||||
tags: initialData?.tags || [],
|
tags: initialData?.tags || [],
|
||||||
@@ -220,6 +280,16 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTextNumberAttributeChange = (attributeId: number, value: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
attributes: prev.attributes.filter(attr => attr.id !== attributeId).concat({
|
||||||
|
id: attributeId,
|
||||||
|
values: value ? [value] : []
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSubmit(formData);
|
onSubmit(formData);
|
||||||
@@ -234,12 +304,26 @@ const Step2Form: FC<Step2FormProps> = ({ onSubmit, loading, onPrevious, category
|
|||||||
{categoryAttributesData.results.data.category.attributes.map((attribute) => {
|
{categoryAttributesData.results.data.category.attributes.map((attribute) => {
|
||||||
const selectedAttribute = formData.attributes.find(attr => attr.id === attribute._id);
|
const selectedAttribute = formData.attributes.find(attr => attr.id === attribute._id);
|
||||||
const selectedValues = selectedAttribute?.values || [];
|
const selectedValues = selectedAttribute?.values || [];
|
||||||
|
const attributeValue = selectedValues.length > 0 ? String(selectedValues[0]) : '';
|
||||||
|
|
||||||
|
// برای attributeهای text، number و boolean از TextNumberInput استفاده کن
|
||||||
|
if (attribute.type === 'text' || attribute.type === 'number' || attribute.type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<TextNumberInput
|
||||||
|
key={attribute._id}
|
||||||
|
attribute={attribute}
|
||||||
|
value={attributeValue}
|
||||||
|
onChange={handleTextNumberAttributeChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// برای attributeهای انتخابی از AttributeSelector استفاده کن
|
||||||
return (
|
return (
|
||||||
<AttributeSelector
|
<AttributeSelector
|
||||||
key={attribute._id}
|
key={attribute._id}
|
||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
selectedValues={selectedValues}
|
selectedValues={selectedValues as number[]}
|
||||||
onChange={handleAttributeChange}
|
onChange={handleAttributeChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type CreateProductDetailResponseType = {
|
|||||||
// مرحله دوم: ویژگیها و توضیحات
|
// مرحله دوم: ویژگیها و توضیحات
|
||||||
export type ProductAttributeType = {
|
export type ProductAttributeType = {
|
||||||
id: number;
|
id: number;
|
||||||
values: number[];
|
values: number[] | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateProductAttributeRequestType = {
|
export type CreateProductAttributeRequestType = {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const validationSchema = Yup.object().shape({
|
|||||||
footerAddress: Yup.string().trim().required('آدرس فوتر الزامی است'),
|
footerAddress: Yup.string().trim().required('آدرس فوتر الزامی است'),
|
||||||
footerDescription: Yup.string().trim().required('توضیحات فوتر الزامی است'),
|
footerDescription: Yup.string().trim().required('توضیحات فوتر الزامی است'),
|
||||||
shopPostalCode: Yup.string().trim(),
|
shopPostalCode: Yup.string().trim(),
|
||||||
siteLogo: Yup.string(),
|
|
||||||
downloadAppDetails: Yup.array(),
|
downloadAppDetails: Yup.array(),
|
||||||
socialMediaLinks: Yup.array()
|
socialMediaLinks: Yup.array()
|
||||||
})
|
})
|
||||||
@@ -50,13 +49,15 @@ const validationSchema = Yup.object().shape({
|
|||||||
const SiteSetting: FC = () => {
|
const SiteSetting: FC = () => {
|
||||||
|
|
||||||
const { data, isLoading } = useGetSiteSetting()
|
const { data, isLoading } = useGetSiteSetting()
|
||||||
const { data: shopData } = useGetShop()
|
const { data: shopData, isLoading: isShopLoading } = useGetShop()
|
||||||
const updateSiteSetting = useUpdateSiteSetting()
|
const updateSiteSetting = useUpdateSiteSetting()
|
||||||
const [isDataLoaded, setIsDataLoaded] = useState(false)
|
const [isDataLoaded, setIsDataLoaded] = useState(false)
|
||||||
const [uploadingIndexes, setUploadingIndexes] = useState<Set<number>>(new Set())
|
const [uploadingIndexes, setUploadingIndexes] = useState<Set<number>>(new Set())
|
||||||
const [pendingFiles, setPendingFiles] = useState<{ [key: string]: File }>({})
|
const [pendingFiles, setPendingFiles] = useState<{ [key: string]: File }>({})
|
||||||
const singleUpload = useSingleUpload()
|
const singleUpload = useSingleUpload()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formik = useFormik<FormValues>({
|
const formik = useFormik<FormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
siteLogo: undefined,
|
siteLogo: undefined,
|
||||||
@@ -119,21 +120,26 @@ const SiteSetting: FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.results?.siteSetting && shopData?.results?.shop) {
|
// وقتی loading تمام شد، دادهها را مقداردهی اولیه کن
|
||||||
|
if (!isLoading && !isShopLoading) {
|
||||||
|
const siteSetting = data?.results?.siteSetting || {}
|
||||||
|
const shop = shopData?.results?.shop || {}
|
||||||
|
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
siteLogo: data.results.siteSetting.siteLogo || undefined,
|
siteLogo: siteSetting.siteLogo || undefined,
|
||||||
footerPhone: data.results.siteSetting.footerPhone || '',
|
footerPhone: siteSetting.footerPhone || '',
|
||||||
footerEmail: data.results.siteSetting.footerEmail || '',
|
footerEmail: siteSetting.footerEmail || '',
|
||||||
footerAddress: data.results.siteSetting.footerAddress || '',
|
footerAddress: siteSetting.footerAddress || '',
|
||||||
footerDescription: data.results.siteSetting.footerDescription || '',
|
footerDescription: siteSetting.footerDescription || '',
|
||||||
shopPostalCode: shopData.results.shop.shopPostalCode || '',
|
shopPostalCode: (shop && 'shopPostalCode' in shop ? String(shop.shopPostalCode) : '') || '',
|
||||||
downloadAppDetails: (data.results.siteSetting.downloadAppDetails || []).map((item: { _id?: string; pic: string; url: string }, index: number) => ({
|
downloadAppDetails: (siteSetting.downloadAppDetails || []).map((item: { _id?: string; pic: string; url: string }, index: number) => ({
|
||||||
id: item._id || `existing-${index}-${Date.now()}`,
|
id: item._id || `existing-${index}-${Date.now()}`,
|
||||||
pic: item.pic,
|
pic: item.pic,
|
||||||
url: item.url
|
url: item.url
|
||||||
})),
|
})),
|
||||||
socialMediaLinks: (data.results.siteSetting.socialMediaLinks || []).map((item: { icon: string; url: string }, index: number) => ({
|
socialMediaLinks: (siteSetting.socialMediaLinks || []).map((item: { icon: string; url: string }, index: number) => ({
|
||||||
id: `existing-${index}-${Date.now()}`,
|
id: `existing-${index}-${Date.now()}`,
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
url: item.url
|
url: item.url
|
||||||
@@ -142,7 +148,7 @@ const SiteSetting: FC = () => {
|
|||||||
setIsDataLoaded(true)
|
setIsDataLoaded(true)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data, shopData])
|
}, [data, shopData, isLoading, isShopLoading])
|
||||||
|
|
||||||
|
|
||||||
const uploadAllPendingFiles = async (): Promise<{ [key: string]: string }> => {
|
const uploadAllPendingFiles = async (): Promise<{ [key: string]: string }> => {
|
||||||
@@ -201,7 +207,9 @@ const SiteSetting: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
|
|
||||||
|
if (isLoading || isShopLoading) {
|
||||||
return <div className="flex justify-center items-center h-64">در حال بارگذاری...</div>
|
return <div className="flex justify-center items-center h-64">در حال بارگذاری...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { useState, type FC } from 'react'
|
||||||
|
import PageTitle from '@/components/PageTitle'
|
||||||
|
import { useDeleteSlider, useGetSliders, useUpdateSlider } from './hooks/useSettingData'
|
||||||
|
import { type SliderItem, type UpdateSliderType } from './types/Types'
|
||||||
|
import PageLoading from '@/components/PageLoading'
|
||||||
|
import Error from '@/components/Error'
|
||||||
|
import Td from '@/components/Td'
|
||||||
|
import { Calendar, Add, Edit, Eye } from 'iconsax-react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import TrashWithConfrim from '@/components/TrashWithConfrim'
|
||||||
|
import Switch from '@/components/Switch'
|
||||||
|
import CreateSliderModal from './components/CreateSliderModal'
|
||||||
|
import EditSliderModal from './components/EditSliderModal'
|
||||||
|
|
||||||
|
const Sliders: FC = () => {
|
||||||
|
const { data, isLoading, error, refetch } = useGetSliders()
|
||||||
|
const updateSlider = useUpdateSlider()
|
||||||
|
const deleteSlider = useDeleteSlider()
|
||||||
|
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||||
|
const [editingSlider, setEditingSlider] = useState<SliderItem | null>(null)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-[400px]">
|
||||||
|
<Error errorText="خطا در بارگذاری اطلاعات اسلایدرها" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sliders = data?.results?.sliders || []
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
return new Date(dateString).toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncateText = (text: string, maxLength: number = 30) => {
|
||||||
|
if (text.length <= maxLength) return text
|
||||||
|
return text.substring(0, maxLength) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDisplayLocationLabel = (location: 'desktop' | 'mobile') => {
|
||||||
|
return location === 'desktop' ? 'دسکتاپ' : 'موبایل'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStatusToggle = (slider: SliderItem) => {
|
||||||
|
const updateData: UpdateSliderType = {
|
||||||
|
displayLocation: slider.displayLocation,
|
||||||
|
imageUrl: slider.imageUrl,
|
||||||
|
altText: slider.altText,
|
||||||
|
title: slider.title,
|
||||||
|
linkUrl: slider.linkUrl,
|
||||||
|
isActive: !slider.isActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSlider.mutate({ id: slider._id, params: updateData })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageTitle />
|
||||||
|
<div className="flex justify-between items-center gap-4 mb-5">
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<h1>مدیریت اسلایدرها</h1>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
|
className='w-fit'
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<Add size={16} color='#fff' />
|
||||||
|
<span>افزودن اسلایدر جدید</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||||
|
<table className='w-full text-sm'>
|
||||||
|
<thead className='thead'>
|
||||||
|
<tr>
|
||||||
|
<Td text={'تصویر'} />
|
||||||
|
<Td text={'عنوان'} />
|
||||||
|
<Td text={'متن جایگزین'} />
|
||||||
|
<Td text={'لینک'} />
|
||||||
|
<Td text={'محل نمایش'} />
|
||||||
|
<Td text={'وضعیت'} />
|
||||||
|
<Td text={'تاریخ ایجاد'} />
|
||||||
|
<Td text={'عملیات'} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sliders.length === 0 ? (
|
||||||
|
<tr className='tr'>
|
||||||
|
<td colSpan={8} className="text-center py-8 text-gray-500">
|
||||||
|
هیچ اسلایدری یافت نشد
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
sliders.map((slider: SliderItem) => (
|
||||||
|
<tr key={slider._id} className='tr'>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={slider.imageUrl}
|
||||||
|
alt={slider.altText}
|
||||||
|
className="max-w-20 max-h-20 object-contain rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<span className="font-medium">{slider.title}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<span className="text-gray-600 text-sm">
|
||||||
|
{truncateText(slider.altText)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<a
|
||||||
|
href={slider.linkUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
{truncateText(slider.linkUrl)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<span className="px-2 py-1 bg-gray-100 rounded text-xs">
|
||||||
|
{getDisplayLocationLabel(slider.displayLocation)}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<Switch
|
||||||
|
active={slider.isActive}
|
||||||
|
onChange={() => handleStatusToggle(slider)}
|
||||||
|
isLoading={updateSlider.isPending}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar size={16} color="#8C90A3" />
|
||||||
|
<span>{formatDate(slider.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingSlider(slider)}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
|
>
|
||||||
|
<Edit size={16} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => {
|
||||||
|
deleteSlider.mutate(slider._id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
isLoading={deleteSlider.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateSliderModal
|
||||||
|
isOpen={isCreateModalOpen}
|
||||||
|
onClose={() => setIsCreateModalOpen(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsCreateModalOpen(false)
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{editingSlider && (
|
||||||
|
<EditSliderModal
|
||||||
|
slider={editingSlider}
|
||||||
|
isOpen={!!editingSlider}
|
||||||
|
onClose={() => setEditingSlider(null)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setEditingSlider(null)
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sliders
|
||||||
|
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import { useCreateSlider } from '../hooks/useSettingData'
|
||||||
|
import { useUploadSingle } from '@/pages/uploader/hooks/useUploaderData'
|
||||||
|
import { type CreateSliderType } from '../types/Types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
import Switch from '@/components/Switch'
|
||||||
|
|
||||||
|
interface CreateSliderModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateSliderModal: FC<CreateSliderModalProps> = ({ isOpen, onClose }) => {
|
||||||
|
const [formData, setFormData] = useState<CreateSliderType>({
|
||||||
|
displayLocation: 'desktop',
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File[]>([])
|
||||||
|
|
||||||
|
const createSliderMutation = useCreateSlider()
|
||||||
|
const uploadSingle = useUploadSingle()
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!formData.title || !formData.altText || !formData.linkUrl) {
|
||||||
|
toast.error('لطفا تمام فیلدهای الزامی را پر کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFile.length === 0) {
|
||||||
|
toast.error('لطفا تصویر اسلایدر را انتخاب کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload image first
|
||||||
|
const uploadResponse = await uploadSingle.mutateAsync(imageFile[0])
|
||||||
|
const imageUrl = uploadResponse.results?.url?.url
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
toast.error('خطا در آپلود تصویر')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create slider with uploaded image URL
|
||||||
|
await createSliderMutation.mutateAsync({
|
||||||
|
displayLocation: formData.displayLocation,
|
||||||
|
imageUrl,
|
||||||
|
altText: formData.altText,
|
||||||
|
title: formData.title,
|
||||||
|
linkUrl: formData.linkUrl,
|
||||||
|
isActive: formData.isActive,
|
||||||
|
}, {
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success('اسلایدر با موفقیت اضافه شد')
|
||||||
|
handleClose()
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('خطا در آپلود تصویر')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setFormData({
|
||||||
|
displayLocation: 'desktop',
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
setImageFile([])
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayLocationOptions = [
|
||||||
|
{ value: 'desktop', label: 'دسکتاپ' },
|
||||||
|
{ value: 'mobile', label: 'موبایل' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal open={isOpen} close={handleClose} isHeader={true} title_header="افزودن اسلایدر جدید">
|
||||||
|
<form onSubmit={handleSubmit} className="w-[500px] mt-4 space-y-6">
|
||||||
|
{/* محل نمایش */}
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
label='محل نمایش'
|
||||||
|
value={formData.displayLocation}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, displayLocation: e.target.value as 'desktop' | 'mobile' }))}
|
||||||
|
items={displayLocationOptions}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* تصویر اسلایدر */}
|
||||||
|
<div>
|
||||||
|
<UploadBox
|
||||||
|
label='تصویر اسلایدر'
|
||||||
|
isMultiple={false}
|
||||||
|
onChange={(files) => setImageFile(files)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* عنوان */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='عنوان اسلایدر'
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||||
|
placeholder="عنوان اسلایدر را وارد کنید"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* متن جایگزین */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='متن جایگزین (Alt Text)'
|
||||||
|
value={formData.altText}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, altText: e.target.value }))}
|
||||||
|
placeholder="توضیح تصویر برای دسترسی"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* لینک */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='لینک مقصد'
|
||||||
|
value={formData.linkUrl}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, linkUrl: e.target.value }))}
|
||||||
|
placeholder="/summer-sale"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* وضعیت */}
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
active={formData.isActive}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, isActive: value }))}
|
||||||
|
label="فعال"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={createSliderMutation.isPending || uploadSingle.isPending}
|
||||||
|
isLoading={createSliderMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
{(createSliderMutation.isPending || uploadSingle.isPending) ? 'در حال ذخیره...' : 'ذخیره'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={createSliderMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateSliderModal
|
||||||
|
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import Input from '@/components/Input'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import { useUpdateSlider } from '../hooks/useSettingData'
|
||||||
|
import { useUploadSingle } from '@/pages/uploader/hooks/useUploaderData'
|
||||||
|
import { type SliderItem, type UpdateSliderType } from '../types/Types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import UploadBox from '@/components/UploadBox'
|
||||||
|
import Switch from '@/components/Switch'
|
||||||
|
|
||||||
|
interface EditSliderModalProps {
|
||||||
|
slider: SliderItem
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditSliderModal: FC<EditSliderModalProps> = ({ slider, isOpen, onClose, onSuccess }) => {
|
||||||
|
const [formData, setFormData] = useState<UpdateSliderType>({
|
||||||
|
displayLocation: 'desktop',
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File[]>([])
|
||||||
|
|
||||||
|
const updateSliderMutation = useUpdateSlider()
|
||||||
|
const uploadSingle = useUploadSingle()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (slider) {
|
||||||
|
setFormData({
|
||||||
|
displayLocation: slider.displayLocation,
|
||||||
|
imageUrl: slider.imageUrl,
|
||||||
|
altText: slider.altText,
|
||||||
|
title: slider.title,
|
||||||
|
linkUrl: slider.linkUrl,
|
||||||
|
isActive: slider.isActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [slider])
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!formData.title || !formData.altText || !formData.linkUrl) {
|
||||||
|
toast.error('لطفا تمام فیلدهای الزامی را پر کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let finalImageUrl = formData.imageUrl
|
||||||
|
|
||||||
|
// Upload new image if selected
|
||||||
|
if (imageFile.length > 0) {
|
||||||
|
const uploadResponse = await uploadSingle.mutateAsync(imageFile[0])
|
||||||
|
finalImageUrl = uploadResponse.results?.url?.url
|
||||||
|
|
||||||
|
if (!finalImageUrl) {
|
||||||
|
toast.error('خطا در آپلود تصویر')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update slider
|
||||||
|
await updateSliderMutation.mutateAsync({
|
||||||
|
id: slider._id,
|
||||||
|
params: {
|
||||||
|
displayLocation: formData.displayLocation,
|
||||||
|
imageUrl: finalImageUrl,
|
||||||
|
altText: formData.altText,
|
||||||
|
title: formData.title,
|
||||||
|
linkUrl: formData.linkUrl,
|
||||||
|
isActive: formData.isActive,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success('اسلایدر با موفقیت بروزرسانی شد')
|
||||||
|
handleClose()
|
||||||
|
onSuccess()
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('خطا در آپلود تصویر')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setFormData({
|
||||||
|
displayLocation: 'desktop',
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
setImageFile([])
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayLocationOptions = [
|
||||||
|
{ value: 'desktop', label: 'دسکتاپ' },
|
||||||
|
{ value: 'mobile', label: 'موبایل' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal open={isOpen} close={handleClose} isHeader={true} title_header="ویرایش اسلایدر">
|
||||||
|
<form onSubmit={handleSubmit} className="w-[500px] mt-4 space-y-6">
|
||||||
|
{/* محل نمایش */}
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
label='محل نمایش'
|
||||||
|
value={formData.displayLocation}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, displayLocation: e.target.value as 'desktop' | 'mobile' }))}
|
||||||
|
items={displayLocationOptions}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* تصویر اسلایدر */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<img
|
||||||
|
src={formData.imageUrl}
|
||||||
|
alt={formData.altText}
|
||||||
|
className="w-full h-32 object-cover rounded border"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
target.src = '/placeholder-image.png'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UploadBox
|
||||||
|
label='تصویر جدید اسلایدر (اختیاری)'
|
||||||
|
isMultiple={false}
|
||||||
|
onChange={(files) => setImageFile(files)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* عنوان */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='عنوان اسلایدر'
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||||
|
placeholder="عنوان اسلایدر را وارد کنید"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* متن جایگزین */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='متن جایگزین (Alt Text)'
|
||||||
|
value={formData.altText}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, altText: e.target.value }))}
|
||||||
|
placeholder="توضیح تصویر برای دسترسی"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* لینک */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='لینک مقصد'
|
||||||
|
value={formData.linkUrl}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, linkUrl: e.target.value }))}
|
||||||
|
placeholder="/summer-sale"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* وضعیت */}
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
active={formData.isActive}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, isActive: value }))}
|
||||||
|
label="فعال"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={updateSliderMutation.isPending || uploadSingle.isPending}
|
||||||
|
isLoading={updateSliderMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
{(updateSliderMutation.isPending || uploadSingle.isPending) ? 'در حال بروزرسانی...' : 'بروزرسانی'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={updateSliderMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditSliderModal
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as api from "../service/SettingService";
|
import * as api from "../service/SettingService";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum";
|
import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum";
|
||||||
import { type CreateFaqType, type CreatePricingItem, type UpdateBannerType } from "../types/Types";
|
import { type CreateFaqType, type CreatePricingItem, type UpdateBannerType, type UpdateSliderType } from "../types/Types";
|
||||||
|
|
||||||
export const useGetAboutUs = () => {
|
export const useGetAboutUs = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -179,3 +179,36 @@ export const useCreateBanner = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSliders = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["sliders"],
|
||||||
|
queryFn: api.getSliders,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSlider = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteSlider,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSlider = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, params}: {id: string, params: UpdateSliderType}) => api.updateSlider(id, params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["sliders"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSlider = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createSlider,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["sliders"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import type {
|
|||||||
CreateDocumentType,
|
CreateDocumentType,
|
||||||
CreateFaqType,
|
CreateFaqType,
|
||||||
CreatePricingItem,
|
CreatePricingItem,
|
||||||
|
CreateSliderType,
|
||||||
DocumentTypesResponse,
|
DocumentTypesResponse,
|
||||||
FaqDetailResponse,
|
FaqDetailResponse,
|
||||||
FaqResponse,
|
FaqResponse,
|
||||||
PricingResponse,
|
PricingResponse,
|
||||||
ShopResponse,
|
ShopResponse,
|
||||||
|
SlidersResponse,
|
||||||
UpdateBannerType,
|
UpdateBannerType,
|
||||||
UpdateShopType,
|
UpdateShopType,
|
||||||
UpdateSiteSetting,
|
UpdateSiteSetting,
|
||||||
|
UpdateSliderType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum";
|
import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum";
|
||||||
|
|
||||||
@@ -138,3 +141,23 @@ export const createBanner = async (params: UpdateBannerType) => {
|
|||||||
const { data } = await axios.post(`/admin/medias/banner`, params);
|
const { data } = await axios.post(`/admin/medias/banner`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSliders = async (): Promise<SlidersResponse> => {
|
||||||
|
const { data } = await axios.get(`/admin/medias/slider`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSlider = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/medias/slider/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSlider = async (id: string, params: UpdateSliderType) => {
|
||||||
|
const { data } = await axios.post(`/admin/medias/slider/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSlider = async (params: CreateSliderType) => {
|
||||||
|
const { data } = await axios.post(`/admin/medias/slider`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -251,3 +251,43 @@ export interface UpdateBannerType {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SliderItem {
|
||||||
|
_id: string;
|
||||||
|
displayLocation: "desktop" | "mobile";
|
||||||
|
imageUrl: string;
|
||||||
|
altText: string;
|
||||||
|
title: string;
|
||||||
|
linkUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlidersResults {
|
||||||
|
sliders: SliderItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlidersResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: SlidersResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSliderType {
|
||||||
|
displayLocation: "desktop" | "mobile";
|
||||||
|
imageUrl: string;
|
||||||
|
altText: string;
|
||||||
|
title: string;
|
||||||
|
linkUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSliderType {
|
||||||
|
displayLocation: "desktop" | "mobile";
|
||||||
|
imageUrl: string;
|
||||||
|
altText: string;
|
||||||
|
title: string;
|
||||||
|
linkUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import ManageDocument from '@/pages/setting/ManageDocument'
|
|||||||
import Pricing from '@/pages/setting/Pricing'
|
import Pricing from '@/pages/setting/Pricing'
|
||||||
import Shop from '@/pages/setting/Shop'
|
import Shop from '@/pages/setting/Shop'
|
||||||
import Banners from '@/pages/setting/Banners'
|
import Banners from '@/pages/setting/Banners'
|
||||||
|
import Sliders from '@/pages/setting/Sliders'
|
||||||
import SiteSetting from '@/pages/setting/SiteSetting'
|
import SiteSetting from '@/pages/setting/SiteSetting'
|
||||||
import JobsList from '@/pages/jobs/List'
|
import JobsList from '@/pages/jobs/List'
|
||||||
import Resumes from '@/pages/jobs/Resume'
|
import Resumes from '@/pages/jobs/Resume'
|
||||||
@@ -161,6 +162,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.pages.incomeTariff} element={<Pricing />} />
|
<Route path={Pages.pages.incomeTariff} element={<Pricing />} />
|
||||||
<Route path={Pages.setting.shop} element={<Shop />} />
|
<Route path={Pages.setting.shop} element={<Shop />} />
|
||||||
<Route path={Pages.setting.banners} element={<Banners />} />
|
<Route path={Pages.setting.banners} element={<Banners />} />
|
||||||
|
<Route path={Pages.setting.sliders} element={<Sliders />} />
|
||||||
<Route path={Pages.setting.siteSetting} element={<SiteSetting />} />
|
<Route path={Pages.setting.siteSetting} element={<SiteSetting />} />
|
||||||
|
|
||||||
<Route path={Pages.jobs.list} element={<JobsList />} />
|
<Route path={Pages.jobs.list} element={<JobsList />} />
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ const SettingsSubMenu: FC = () => {
|
|||||||
<SubMenuItem
|
<SubMenuItem
|
||||||
title={'تنظیمات بنرها'}
|
title={'تنظیمات بنرها'}
|
||||||
isActive={isActive('banners')}
|
isActive={isActive('banners')}
|
||||||
link="/settings/banners"
|
link={Pages.setting.banners}
|
||||||
|
/>
|
||||||
|
<SubMenuItem
|
||||||
|
title={'تنظیمات اسلایدرها'}
|
||||||
|
isActive={isActive('sliders')}
|
||||||
|
link={Pages.setting.sliders}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user