setting footer

This commit is contained in:
hamid zarghami
2025-10-01 12:04:55 +03:30
parent 45f44028f3
commit 2d52076ac0
12 changed files with 570 additions and 16 deletions
+1
View File
@@ -68,6 +68,7 @@ export const Pages = {
setting: { setting: {
shop: "/settings/shop", shop: "/settings/shop",
banners: "/settings/banners", banners: "/settings/banners",
siteSetting: "/settings/site-setting",
}, },
wallet: "/wallet", wallet: "/wallet",
profile: "/profile", profile: "/profile",
+2 -7
View File
@@ -64,7 +64,7 @@ const Banners: FC = () => {
<div> <div>
<div className="flex justify-between items-center gap-4 mb-5"> <div className="flex justify-between items-center gap-4 mb-5">
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<h1 className='text-xl font-semibold'>مدیریت بنرها</h1> <h1>مدیریت بنرها</h1>
</div> </div>
<Button <Button
onClick={() => setIsCreateModalOpen(true)} onClick={() => setIsCreateModalOpen(true)}
@@ -139,17 +139,12 @@ const Banners: FC = () => {
</a> </a>
</div> </div>
</Td> </Td>
<Td text=""> <Td text={banner.order.toString()} />
<div className="flex items-center">
<span className="font-medium text-lg">{banner.order}</span>
</div>
</Td>
<Td text=""> <Td text="">
<Switch <Switch
active={banner.isActive} active={banner.isActive}
onChange={() => handleStatusToggle(banner)} onChange={() => handleStatusToggle(banner)}
isLoading={updateBanner.isPending} isLoading={updateBanner.isPending}
label={banner.isActive ? 'فعال' : 'غیرفعال'}
/> />
</Td> </Td>
<Td text=""> <Td text="">
+193 -2
View File
@@ -1,8 +1,199 @@
import { type FC } from 'react' import { type FC, useEffect, useState } from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useGetSiteSetting, useUpdateSiteSetting, useGetShop } from './hooks/useSettingData'
import { uploadSingle } from '../uploader/service/UploaderService'
type DownloadAppDetail = {
id: string
pic: string
url: string
}
type FormValues = {
siteLogo?: string
footerPhone: string
footerEmail: string
footerAddress: string
footerDescription: string
shopPostalCode?: string
downloadAppDetails: DownloadAppDetail[]
socialMediaLinks: { icon: string; url: string }[]
}
import Button from '../../components/Button'
import { TickCircle } from 'iconsax-react'
import MainInfoSection from './components/MainInfoSection'
import FilesSection from './components/FilesSection'
import DownloadAppSection from './components/DownloadAppSection'
import SocialMediaSection from './components/SocialMediaSection'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '../../helpers/utils'
const validationSchema = Yup.object().shape({
footerPhone: Yup.string().trim().required('تلفن فوتر الزامی است'),
footerEmail: Yup.string().email('ایمیل معتبر نیست').required('ایمیل فوتر الزامی است'),
footerAddress: Yup.string().trim().required('آدرس فوتر الزامی است'),
footerDescription: Yup.string().trim().required('توضیحات فوتر الزامی است'),
shopPostalCode: Yup.string().trim(),
siteLogo: Yup.string(),
downloadAppDetails: Yup.array(),
socialMediaLinks: Yup.array()
})
const SiteSetting: FC = () => { const SiteSetting: FC = () => {
const { data, isLoading } = useGetSiteSetting()
const { data: shopData } = useGetShop()
const updateSiteSetting = useUpdateSiteSetting()
const [isDataLoaded, setIsDataLoaded] = useState(false)
const [uploadingIndexes, setUploadingIndexes] = useState<Set<number>>(new Set())
const [pendingFiles, setPendingFiles] = useState<{ [key: string]: File }>({})
const formik = useFormik<FormValues>({
initialValues: {
siteLogo: undefined,
footerPhone: '',
footerEmail: '',
footerAddress: '',
footerDescription: '',
shopPostalCode: '',
downloadAppDetails: [],
socialMediaLinks: []
},
validationSchema,
validateOnChange: false,
validateOnBlur: false,
onSubmit: async (values) => {
// اول همه فایل‌های pending رو آپلود کن
if (Object.keys(pendingFiles).length > 0) {
await uploadAllPendingFiles()
// پاک کردن pending files بعد از آپلود موفق
setPendingFiles({})
}
// transform data برای ارسال به API (حذف id)
const transformedValues = {
...values,
downloadAppDetails: values.downloadAppDetails.map((item) => ({
pic: item.pic,
url: item.url
}))
}
// حالا فرم رو submit کن
updateSiteSetting.mutate(transformedValues, {
onSuccess: () => {
toast.success('تنظیمات سایت با موفقیت بروزرسانی شد')
// پاک کردن pending files بعد از submit موفق
setPendingFiles({})
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
}
})
useEffect(() => {
if (data?.results?.siteSetting && shopData?.results?.shop) {
formik.setValues({
siteLogo: data.results.siteSetting.siteLogo || undefined,
footerPhone: data.results.siteSetting.footerPhone || '',
footerEmail: data.results.siteSetting.footerEmail || '',
footerAddress: data.results.siteSetting.footerAddress || '',
footerDescription: data.results.siteSetting.footerDescription || '',
shopPostalCode: shopData.results.shop.shopPostalCode || '',
downloadAppDetails: (data.results.siteSetting.downloadAppDetails || []).map((item: { _id?: string; pic: string; url: string }, index: number) => ({
id: item._id || `existing-${index}-${Date.now()}`,
pic: item.pic,
url: item.url
})),
socialMediaLinks: data.results.siteSetting.socialMediaLinks || []
}, false)
setIsDataLoaded(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, shopData])
const uploadAllPendingFiles = async (): Promise<{ [key: string]: string }> => {
const uploadedUrls: { [key: string]: string } = {}
for (const [id, file] of Object.entries(pendingFiles)) {
// پیدا کردن index آیتم بر اساس id
const index = formik.values.downloadAppDetails?.findIndex(item => item.id === id)
if (index === undefined || index === -1) continue
try {
setUploadingIndexes(prev => new Set(prev).add(index))
const uploadResponse = await uploadSingle(file)
const imageUrl = uploadResponse.results.url
uploadedUrls[id] = imageUrl
// بروزرسانی formik
const currentDetails = formik.values.downloadAppDetails || []
const updatedDetails = currentDetails.map((item, i) =>
i === index ? { ...item, pic: imageUrl } : item
)
formik.setFieldValue('downloadAppDetails', updatedDetails)
} catch (error) {
toast.error(`خطا در آپلود تصویر آیتم ${index + 1}`)
throw error
} finally {
setUploadingIndexes(prev => {
const newSet = new Set(prev)
newSet.delete(index)
return newSet
})
}
}
return uploadedUrls
}
if (isLoading) {
return <div className="flex justify-center items-center h-64">در حال بارگذاری...</div>
}
return ( return (
<div>SiteSetting</div> <div className='mt-4'>
<div className='flex justify-between items-center'>
<div className='flex items-center gap-3'>
<h1>تنظیمات سایت</h1>
</div>
<div>
<Button
className='px-6'
onClick={() => formik.handleSubmit()}
isLoading={updateSiteSetting.isPending}
disabled={(!isDataLoaded || !formik.isValid) || updateSiteSetting.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={16} color='#fff' />
<div>بروزرسانی تنظیمات</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-6'>
<MainInfoSection formik={formik} isDataLoaded={isDataLoaded} />
<FilesSection formik={formik} />
</div>
<DownloadAppSection
formik={formik}
pendingFiles={pendingFiles}
setPendingFiles={setPendingFiles}
uploadingIndexes={uploadingIndexes}
setUploadingIndexes={setUploadingIndexes}
/>
<SocialMediaSection formik={formik} />
</div>
) )
} }
@@ -0,0 +1,140 @@
import { type FC } from 'react'
import type { FormikProps } from 'formik'
import { Add, Trash } from 'iconsax-react'
import Button from '../../../components/Button'
import Input from '../../../components/Input'
import UploadBox from '../../../components/UploadBox'
type DownloadAppDetail = {
id: string
pic: string
url: string
}
type FormValues = {
siteLogo?: string
footerPhone: string
footerEmail: string
footerAddress: string
footerDescription: string
shopPostalCode?: string
downloadAppDetails: DownloadAppDetail[]
socialMediaLinks: { icon: string; url: string }[]
}
type Props = {
formik: FormikProps<FormValues>
pendingFiles: { [key: string]: File }
setPendingFiles: React.Dispatch<React.SetStateAction<{ [key: string]: File }>>
uploadingIndexes: Set<number>
setUploadingIndexes: React.Dispatch<React.SetStateAction<Set<number>>>
}
const DownloadAppSection: FC<Props> = ({
formik,
pendingFiles,
setPendingFiles,
uploadingIndexes,
setUploadingIndexes
}) => {
const addDownloadAppDetail = () => {
const currentDetails = formik.values.downloadAppDetails || []
const newId = Date.now().toString()
formik.setFieldValue('downloadAppDetails', [...currentDetails, { id: newId, pic: '', url: '' }])
}
const removeDownloadAppDetail = (index: number) => {
const currentDetails = formik.values.downloadAppDetails || []
const itemToRemove = currentDetails[index]
formik.setFieldValue('downloadAppDetails', currentDetails.filter((_, i) => i !== index))
// پاک کردن pending file مربوطه
if (itemToRemove) {
setPendingFiles(prev => {
const newPending = { ...prev }
delete newPending[itemToRemove.id]
return newPending
})
}
}
const updateDownloadAppDetail = (index: number, field: 'pic' | 'url', value: string) => {
const currentDetails = formik.values.downloadAppDetails || []
const updatedDetails = currentDetails.map((item, i) =>
i === index ? { ...item, [field]: value } : item
)
formik.setFieldValue('downloadAppDetails', updatedDetails)
}
const handleDownloadAppImageUpload = (index: number, files: File[]) => {
if (files.length === 0) return
const item = formik.values.downloadAppDetails?.[index]
if (!item) return
// ذخیره فایل برای آپلود بعدی
setPendingFiles(prev => ({
...prev,
[item.id]: files[0]
}))
// نمایش preview
updateDownloadAppDetail(index, 'pic', URL.createObjectURL(files[0]))
}
return (
<div className='mt-6 bg-white py-8 xl:px-10 px-6 rounded-3xl'>
<div className='flex justify-between items-center mb-6'>
<h2 className='text-lg font-medium'>جزئیات دانلود اپ</h2>
<Button
size="sm"
variant="outline"
onClick={addDownloadAppDetail}
className="flex items-center gap-2 w-fit"
>
<Add size={16} color='#6B7280' />
افزودن
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{formik.values.downloadAppDetails?.map((item, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex justify-between items-center mb-4">
<span className="text-sm font-medium">آیتم {index + 1}</span>
<Button className='w-fit' size="sm" variant="danger" onClick={() => removeDownloadAppDetail(index)}>
<Trash size={16} color='white' />
</Button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">تصویر</label>
<UploadBox
label=""
isMultiple={false}
onChange={(files) => handleDownloadAppImageUpload(index, files)}
isReset={false}
/>
{item.pic && (
<img src={item.pic} alt={`تصویر ${index + 1}`} className="mt-2 w-20 h-20 object-contain rounded" />
)}
{uploadingIndexes.has(index) && (
<div className="mt-2 text-sm text-blue-600">در حال آپلود...</div>
)}
</div>
<Input
label="لینک"
value={item.url}
onChange={(e) => updateDownloadAppDetail(index, 'url', e.target.value)}
placeholder="URL دانلود"
/>
</div>
</div>
))}
</div>
</div>
)
}
export default DownloadAppSection
@@ -0,0 +1,64 @@
import { type FC } from 'react'
import type { FormikProps } from 'formik'
import UploadBox from '../../../components/UploadBox'
type FormValues = {
siteLogo?: string
footerPhone: string
footerEmail: string
footerAddress: string
footerDescription: string
shopPostalCode?: string
downloadAppDetails: { id: string; pic: string; url: string }[]
socialMediaLinks: { icon: string; url: string }[]
}
type Props = {
formik: FormikProps<FormValues>
}
const FilesSection: FC<Props> = ({ formik }) => {
return (
<div className='w-80 space-y-6'>
<div className='text-sm bg-white py-8 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>فایلها</h2>
<div className='space-y-6'>
<div>
<label className="block text-sm font-medium mb-2">لوگو سایت</label>
<UploadBox
label=""
isMultiple={false}
onChange={(files) => {
if (files.length > 0) {
formik.setFieldValue('siteLogo', URL.createObjectURL(files[0]))
}
}}
/>
{formik.values.siteLogo && (
<img src={formik.values.siteLogo} alt="لوگو سایت" className="mt-2 w-20 h-20 object-contain rounded" />
)}
</div>
<div>
<label className="block text-sm font-medium mb-2">کد پستی فروشگاه</label>
<UploadBox
label=""
isMultiple={false}
onChange={(files) => {
if (files.length > 0) {
formik.setFieldValue('shopPostalCode', URL.createObjectURL(files[0]))
}
}}
/>
{formik.values.shopPostalCode && (
<img src={formik.values.shopPostalCode} alt="کد پستی فروشگاه" className="mt-2 w-20 h-20 object-contain rounded" />
)}
</div>
</div>
</div>
</div>
)
}
export default FilesSection
@@ -0,0 +1,64 @@
import { type FC } from 'react'
import type { FormikProps } from 'formik'
import Input from '../../../components/Input'
import Textarea from '../../../components/Textarea'
import TextEditor from '../../../components/TextEditor'
type FormValues = {
siteLogo?: string
footerPhone: string
footerEmail: string
footerAddress: string
footerDescription: string
shopPostalCode?: string
downloadAppDetails: { id: string; pic: string; url: string }[]
socialMediaLinks: { icon: string; url: string }[]
}
type Props = {
formik: FormikProps<FormValues>
isDataLoaded: boolean
}
const MainInfoSection: FC<Props> = ({ formik, isDataLoaded }) => {
return (
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>اطلاعات اصلی</h2>
<div className='space-y-6'>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='تلفن فوتر'
placeholder='مثال: ۵۷۲۷ - ۹۴۵ - ۰۹۳۹'
{...formik.getFieldProps('footerPhone')}
error_text={isDataLoaded && formik.touched.footerPhone && formik.errors.footerPhone ? formik.errors.footerPhone : ''}
/>
<Input
label='ایمیل فوتر'
placeholder='مثال: info@shinan.ir'
{...formik.getFieldProps('footerEmail')}
error_text={isDataLoaded && formik.touched.footerEmail && formik.errors.footerEmail ? formik.errors.footerEmail : ''}
/>
</div>
<Textarea
label='آدرس فوتر'
placeholder='آدرس کامل را وارد کنید...'
{...formik.getFieldProps('footerAddress')}
error_text={isDataLoaded && formik.touched.footerAddress && formik.errors.footerAddress ? formik.errors.footerAddress : ''}
/>
<div>
<label className="block text-sm font-medium mb-2">توضیحات فوتر</label>
<TextEditor
value={formik.values.footerDescription || ''}
onChange={(value) => formik.setFieldValue('footerDescription', value)}
height="h-32"
/>
</div>
</div>
</div>
)
}
export default MainInfoSection
@@ -0,0 +1,86 @@
import { type FC } from 'react'
import type { FormikProps } from 'formik'
import { Add, Trash } from 'iconsax-react'
import Button from '../../../components/Button'
import Input from '../../../components/Input'
type FormValues = {
siteLogo?: string
footerPhone: string
footerEmail: string
footerAddress: string
footerDescription: string
shopPostalCode?: string
downloadAppDetails: { id: string; pic: string; url: string }[]
socialMediaLinks: { icon: string; url: string }[]
}
type Props = {
formik: FormikProps<FormValues>
}
const SocialMediaSection: FC<Props> = ({ formik }) => {
const addSocialMediaLink = () => {
const currentLinks = formik.values.socialMediaLinks || []
formik.setFieldValue('socialMediaLinks', [...currentLinks, { icon: '', url: '' }])
}
const removeSocialMediaLink = (index: number) => {
const currentLinks = formik.values.socialMediaLinks || []
formik.setFieldValue('socialMediaLinks', currentLinks.filter((_, i) => i !== index))
}
const updateSocialMediaLink = (index: number, field: 'icon' | 'url', value: string) => {
const currentLinks = formik.values.socialMediaLinks || []
const updatedLinks = currentLinks.map((item, i) =>
i === index ? { ...item, [field]: value } : item
)
formik.setFieldValue('socialMediaLinks', updatedLinks)
}
return (
<div className='mt-6 bg-white py-8 xl:px-10 px-6 rounded-3xl'>
<div className='flex justify-between items-center mb-6'>
<h2 className='text-lg font-medium'>لینکهای رسانههای اجتماعی</h2>
<Button
size="sm"
variant="outline"
onClick={addSocialMediaLink}
className="flex items-center gap-2 w-fit"
>
<Add size={16} color='#6B7280' />
افزودن
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{formik.values.socialMediaLinks?.map((item, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex justify-between items-center mb-4">
<span className="text-sm font-medium">لینک {index + 1}</span>
<Button className='w-fit' size="sm" variant="danger" onClick={() => removeSocialMediaLink(index)}>
<Trash size={16} color='white' />
</Button>
</div>
<div className="space-y-4">
<Input
label="آیکون"
value={item.icon}
onChange={(e) => updateSocialMediaLink(index, 'icon', e.target.value)}
placeholder="مثال: telegram"
/>
<Input
label="لینک"
value={item.url}
onChange={(e) => updateSocialMediaLink(index, 'url', e.target.value)}
placeholder="URL صفحه"
/>
</div>
</div>
))}
</div>
</div>
)
}
export default SocialMediaSection
+3 -3
View File
@@ -59,11 +59,11 @@ export const useUpdateFaq = () => {
}); });
}; };
export const useGetSiteSetting = (page: SiteSettingPageEnum) => { export const useGetSiteSetting = (page?: SiteSettingPageEnum) => {
return useQuery({ return useQuery({
queryKey: ["siteSetting", page], queryKey: page ? ["siteSetting", page] : ["siteSetting"],
queryFn: () => api.getSiteSetting(page), queryFn: () => api.getSiteSetting(page),
enabled: !!page, enabled: true,
}); });
}; };
+3 -2
View File
@@ -61,10 +61,11 @@ export const updateFaq = async (id: string, params: CreateFaqType) => {
}; };
export const getSiteSetting = async ( export const getSiteSetting = async (
page: SiteSettingPageEnum page?: SiteSettingPageEnum
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> => { ): Promise<any> => {
const { data } = await axios.get(`/site-setting?name=${page}`); const url = page ? `/site-setting?name=${page}` : `/site-setting`;
const { data } = await axios.get(url);
return data; return data;
}; };
+10
View File
@@ -75,11 +75,16 @@ export interface SiteSetting {
downloadAppDetails?: { downloadAppDetails?: {
pic: string; pic: string;
url: string; url: string;
_id?: string;
id?: string;
}[]; }[];
socialMediaLinks?: { socialMediaLinks?: {
icon: string; icon: string;
url: string; url: string;
_id?: string;
id?: string;
}[]; }[];
shopPostalCode?: string;
} }
export interface SiteSettingResults { export interface SiteSettingResults {
@@ -106,11 +111,16 @@ export interface UpdateSiteSetting {
downloadAppDetails?: { downloadAppDetails?: {
pic: string; pic: string;
url: string; url: string;
_id?: string;
id?: string;
}[]; }[];
socialMediaLinks?: { socialMediaLinks?: {
icon: string; icon: string;
url: string; url: string;
_id?: string;
id?: string;
}[]; }[];
shopPostalCode?: string;
} }
export interface CreateDocumentType { export interface CreateDocumentType {
+2
View File
@@ -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 SiteSetting from '@/pages/setting/SiteSetting'
const MainRouter: FC = () => { const MainRouter: FC = () => {
@@ -139,6 +140,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.siteSetting} element={<SiteSetting />} />
</Routes> </Routes>
</div> </div>
</div> </div>
+2 -2
View File
@@ -766,8 +766,8 @@ const SettingsSubMenu: FC = () => {
/> />
<SubMenuItem <SubMenuItem
title={'تنظیمات فوتر'} title={'تنظیمات فوتر'}
isActive={isActive('footer')} isActive={isActive('site-setting')}
link="/settings/footer" link={Pages.setting.siteSetting}
/> />
<SubMenuItem <SubMenuItem
title={'تنظیمات بنرها'} title={'تنظیمات بنرها'}