banner
This commit is contained in:
@@ -57,7 +57,7 @@ const UploadBox: FC<Props> = (props: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>{props.label}</div>
|
<div className='text-sm'>{props.label}</div>
|
||||||
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
<button {...getRootProps()} type="button" className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
{t('select_file')}
|
{t('select_file')}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const Pages = {
|
|||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
shop: "/settings/shop",
|
shop: "/settings/shop",
|
||||||
|
banners: "/settings/banners",
|
||||||
},
|
},
|
||||||
wallet: "/wallet",
|
wallet: "/wallet",
|
||||||
profile: "/profile",
|
profile: "/profile",
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { useState, type FC } from 'react'
|
||||||
|
import { useDeleteBanner, useGetBanners, useUpdateBanner } from './hooks/useSettingData'
|
||||||
|
import { type BannerItem, type UpdateBannerType } 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 CreateBannerModal from './components/CreateBannerModal'
|
||||||
|
import EditBannerModal from './components/EditBannerModal'
|
||||||
|
|
||||||
|
const Banners: FC = () => {
|
||||||
|
const { data, isLoading, error, refetch } = useGetBanners()
|
||||||
|
const updateBanner = useUpdateBanner()
|
||||||
|
const deleteBanner = useDeleteBanner()
|
||||||
|
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
|
||||||
|
const [editingBanner, setEditingBanner] = useState<BannerItem | 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 banners = data?.results?.banners || []
|
||||||
|
|
||||||
|
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 handleStatusToggle = (banner: BannerItem) => {
|
||||||
|
const updateData: UpdateBannerType = {
|
||||||
|
imageUrl: banner.imageUrl,
|
||||||
|
altText: banner.altText,
|
||||||
|
title: banner.title,
|
||||||
|
linkUrl: banner.linkUrl,
|
||||||
|
isActive: !banner.isActive,
|
||||||
|
order: banner.order
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBanner.mutate({ id: banner._id, params: updateData })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center gap-4 mb-5">
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<h1 className='text-xl font-semibold'>مدیریت بنرها</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>
|
||||||
|
{banners.length === 0 ? (
|
||||||
|
<tr className='tr'>
|
||||||
|
<td colSpan={8} className="text-center py-8 text-gray-500">
|
||||||
|
هیچ بنری یافت نشد
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
banners.map((banner: BannerItem) => (
|
||||||
|
<tr key={banner._id} className='tr'>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={banner.imageUrl}
|
||||||
|
alt={banner.altText}
|
||||||
|
className="max-w-20 max-h-20 object-contain rounded"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
target.src = '/placeholder-image.png'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<span className="font-medium">{banner.title}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<span className="text-gray-600 text-sm">
|
||||||
|
{truncateText(banner.altText)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<a
|
||||||
|
href={banner.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(banner.linkUrl)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium text-lg">{banner.order}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<Switch
|
||||||
|
active={banner.isActive}
|
||||||
|
onChange={() => handleStatusToggle(banner)}
|
||||||
|
isLoading={updateBanner.isPending}
|
||||||
|
label={banner.isActive ? 'فعال' : 'غیرفعال'}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar size={16} color="#8C90A3" />
|
||||||
|
<span>{formatDate(banner.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td text="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingBanner(banner)}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
|
>
|
||||||
|
<Edit size={16} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
<TrashWithConfrim
|
||||||
|
onDelete={() => {
|
||||||
|
deleteBanner.mutate(banner._id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
isLoading={deleteBanner.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateBannerModal
|
||||||
|
isOpen={isCreateModalOpen}
|
||||||
|
onClose={() => setIsCreateModalOpen(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsCreateModalOpen(false)
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{editingBanner && (
|
||||||
|
<EditBannerModal
|
||||||
|
banner={editingBanner}
|
||||||
|
isOpen={!!editingBanner}
|
||||||
|
onClose={() => setEditingBanner(null)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setEditingBanner(null)
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Banners
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
const SiteSetting: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>SiteSetting</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SiteSetting
|
||||||
@@ -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 { useCreateBanner } from '../hooks/useSettingData'
|
||||||
|
import { useUploadSingle } from '../../../pages/uploader/hooks/useUploaderData'
|
||||||
|
import { type CreateBannerType } from '../types/Types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import UploadBox from '../../../components/UploadBox'
|
||||||
|
import Switch from '../../../components/Switch'
|
||||||
|
|
||||||
|
interface CreateBannerModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateBannerModal: FC<CreateBannerModalProps> = ({ isOpen, onClose }) => {
|
||||||
|
const [formData, setFormData] = useState<CreateBannerType>({
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
order: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File[]>([])
|
||||||
|
|
||||||
|
const createBannerMutation = useCreateBanner()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.order || formData.order < 1) {
|
||||||
|
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 banner with uploaded image URL
|
||||||
|
await createBannerMutation.mutateAsync({
|
||||||
|
imageUrl,
|
||||||
|
altText: formData.altText,
|
||||||
|
title: formData.title,
|
||||||
|
linkUrl: formData.linkUrl,
|
||||||
|
isActive: formData.isActive,
|
||||||
|
order: formData.order,
|
||||||
|
}, {
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success('بنر با موفقیت اضافه شد')
|
||||||
|
handleClose()
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('خطا در آپلود تصویر')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setFormData({
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
order: 1,
|
||||||
|
})
|
||||||
|
setImageFile([])
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal open={isOpen} close={handleClose} isHeader={true} title_header="افزودن بنر جدید">
|
||||||
|
<form onSubmit={handleSubmit} className="w-[500px] mt-4 space-y-6">
|
||||||
|
{/* تصویر بنر */}
|
||||||
|
<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="https://example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ترتیب نمایش */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='ترتیب نمایش'
|
||||||
|
type='number'
|
||||||
|
value={formData.order}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 1 }))}
|
||||||
|
placeholder="1"
|
||||||
|
min={1}
|
||||||
|
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={createBannerMutation.isPending || uploadSingle.isPending}
|
||||||
|
isLoading={createBannerMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
{(createBannerMutation.isPending || uploadSingle.isPending) ? 'در حال ذخیره...' : 'ذخیره'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={createBannerMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateBannerModal
|
||||||
@@ -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 { useUpdateBanner } from '../hooks/useSettingData'
|
||||||
|
import { useUploadSingle } from '../../../pages/uploader/hooks/useUploaderData'
|
||||||
|
import { type BannerItem, type UpdateBannerType } from '../types/Types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { extractErrorMessage } from '@/helpers/utils'
|
||||||
|
import UploadBox from '../../../components/UploadBox'
|
||||||
|
import Switch from '../../../components/Switch'
|
||||||
|
|
||||||
|
interface EditBannerModalProps {
|
||||||
|
banner: BannerItem
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSuccess: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditBannerModal: FC<EditBannerModalProps> = ({ banner, isOpen, onClose, onSuccess }) => {
|
||||||
|
const [formData, setFormData] = useState<UpdateBannerType>({
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
order: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [imageFile, setImageFile] = useState<File[]>([])
|
||||||
|
|
||||||
|
const updateBannerMutation = useUpdateBanner()
|
||||||
|
const uploadSingle = useUploadSingle()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (banner) {
|
||||||
|
setFormData({
|
||||||
|
imageUrl: banner.imageUrl,
|
||||||
|
altText: banner.altText,
|
||||||
|
title: banner.title,
|
||||||
|
linkUrl: banner.linkUrl,
|
||||||
|
isActive: banner.isActive,
|
||||||
|
order: banner.order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [banner])
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!formData.title || !formData.altText || !formData.linkUrl) {
|
||||||
|
toast.error('لطفا تمام فیلدهای الزامی را پر کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.order || formData.order < 1) {
|
||||||
|
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 banner
|
||||||
|
await updateBannerMutation.mutateAsync({
|
||||||
|
id: banner._id,
|
||||||
|
params: {
|
||||||
|
imageUrl: finalImageUrl,
|
||||||
|
altText: formData.altText,
|
||||||
|
title: formData.title,
|
||||||
|
linkUrl: formData.linkUrl,
|
||||||
|
isActive: formData.isActive,
|
||||||
|
order: formData.order,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success('بنر با موفقیت بروزرسانی شد')
|
||||||
|
handleClose()
|
||||||
|
onSuccess()
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('خطا در آپلود تصویر')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setFormData({
|
||||||
|
imageUrl: '',
|
||||||
|
altText: '',
|
||||||
|
title: '',
|
||||||
|
linkUrl: '',
|
||||||
|
isActive: true,
|
||||||
|
order: 1,
|
||||||
|
})
|
||||||
|
setImageFile([])
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal open={isOpen} close={handleClose} isHeader={true} title_header="ویرایش بنر">
|
||||||
|
<form onSubmit={handleSubmit} className="w-[500px] mt-4 space-y-6">
|
||||||
|
{/* تصویر بنر */}
|
||||||
|
<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="https://example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ترتیب نمایش */}
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
label='ترتیب نمایش'
|
||||||
|
type='number'
|
||||||
|
value={formData.order}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 1 }))}
|
||||||
|
placeholder="1"
|
||||||
|
min={1}
|
||||||
|
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={updateBannerMutation.isPending || uploadSingle.isPending}
|
||||||
|
isLoading={updateBannerMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
{(updateBannerMutation.isPending || uploadSingle.isPending) ? 'در حال بروزرسانی...' : 'بروزرسانی'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={updateBannerMutation.isPending || uploadSingle.isPending}
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditBannerModal
|
||||||
@@ -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 } from "../types/Types";
|
import { type CreateFaqType, type CreatePricingItem, type UpdateBannerType } from "../types/Types";
|
||||||
|
|
||||||
export const useGetAboutUs = () => {
|
export const useGetAboutUs = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -144,4 +144,38 @@ export const useUpdateShop = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["shop"] });
|
queryClient.invalidateQueries({ queryKey: ["shop"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetBanners = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["banners"],
|
||||||
|
queryFn: api.getBanners,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteBanner = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.deleteBanner,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateBanner = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, params}: {id: string, params: UpdateBannerType}) => api.updateBanner(id, params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["banners"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateBanner = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.createBanner,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["banners"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from "../../../config/axios";
|
import axios from "../../../config/axios";
|
||||||
import type {
|
import type {
|
||||||
AboutUsResponse,
|
AboutUsResponse,
|
||||||
|
BannersResponse,
|
||||||
CreateAboutUs,
|
CreateAboutUs,
|
||||||
CreateDocumentType,
|
CreateDocumentType,
|
||||||
CreateFaqType,
|
CreateFaqType,
|
||||||
@@ -10,6 +11,7 @@ import type {
|
|||||||
FaqResponse,
|
FaqResponse,
|
||||||
PricingResponse,
|
PricingResponse,
|
||||||
ShopResponse,
|
ShopResponse,
|
||||||
|
UpdateBannerType,
|
||||||
UpdateShopType,
|
UpdateShopType,
|
||||||
UpdateSiteSetting,
|
UpdateSiteSetting,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
@@ -115,3 +117,23 @@ export const updateShop = async (params: UpdateShopType) => {
|
|||||||
const { data } = await axios.patch(`/admin/shop`, params);
|
const { data } = await axios.patch(`/admin/shop`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBanners = async (): Promise<BannersResponse> => {
|
||||||
|
const { data } = await axios.get(`/admin/medias/banners`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteBanner = async (id: string) => {
|
||||||
|
const { data } = await axios.delete(`/admin/medias/banner/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBanner = async (id: string, params: UpdateBannerType) => {
|
||||||
|
const { data } = await axios.post(`/admin/medias/banner/${id}`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createBanner = async (params: UpdateBannerType) => {
|
||||||
|
const { data } = await axios.post(`/admin/medias/banner`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -201,3 +201,43 @@ export interface UpdateShopType {
|
|||||||
shopPostalCode: string;
|
shopPostalCode: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BannerItem {
|
||||||
|
_id: string;
|
||||||
|
imageUrl: string;
|
||||||
|
altText: string;
|
||||||
|
title: string;
|
||||||
|
linkUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
order: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BannersResults {
|
||||||
|
banners: BannerItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BannersResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: BannersResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateBannerType {
|
||||||
|
imageUrl: string;
|
||||||
|
altText: string;
|
||||||
|
title: string;
|
||||||
|
linkUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateBannerType {
|
||||||
|
imageUrl: string;
|
||||||
|
altText: string;
|
||||||
|
title: string;
|
||||||
|
linkUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import JobsPage from '@/pages/setting/JobsPage'
|
|||||||
import ManageDocument from '@/pages/setting/ManageDocument'
|
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'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -137,6 +138,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.pages.documents} element={<ManageDocument />} />
|
<Route path={Pages.pages.documents} element={<ManageDocument />} />
|
||||||
<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 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user