diff --git a/src/components/UploadBox.tsx b/src/components/UploadBox.tsx index 7df13fc..b23f0d3 100644 --- a/src/components/UploadBox.tsx +++ b/src/components/UploadBox.tsx @@ -57,7 +57,7 @@ const UploadBox: FC = (props: Props) => {
{props.label}
- +
+ +
+ + + + + + + {banners.length === 0 ? ( + + + + ) : ( + banners.map((banner: BannerItem) => ( + + + + + + + + + + + )) + )} + +
+ + + + + + + +
+ هیچ بنری یافت نشد +
+
+ {banner.altText} { + const target = e.target as HTMLImageElement + target.src = '/placeholder-image.png' + }} + /> +
+
+
+ {banner.title} +
+
+
+ + {truncateText(banner.altText)} + +
+
+ + +
+ {banner.order} +
+
+ handleStatusToggle(banner)} + isLoading={updateBanner.isPending} + label={banner.isActive ? 'فعال' : 'غیرفعال'} + /> + +
+ + {formatDate(banner.createdAt)} +
+
+
+ + { + deleteBanner.mutate(banner._id, { + onSuccess: () => { + refetch() + } + }) + }} + isLoading={deleteBanner.isPending} + /> +
+
+
+ + setIsCreateModalOpen(false)} + onSuccess={() => { + setIsCreateModalOpen(false) + refetch() + }} + /> + + {editingBanner && ( + setEditingBanner(null)} + onSuccess={() => { + setEditingBanner(null) + refetch() + }} + /> + )} +
+ ) +} + +export default Banners \ No newline at end of file diff --git a/src/pages/setting/SiteSetting.tsx b/src/pages/setting/SiteSetting.tsx new file mode 100644 index 0000000..fdbe0bc --- /dev/null +++ b/src/pages/setting/SiteSetting.tsx @@ -0,0 +1,9 @@ +import { type FC } from 'react' + +const SiteSetting: FC = () => { + return ( +
SiteSetting
+ ) +} + +export default SiteSetting \ No newline at end of file diff --git a/src/pages/setting/components/CreateBannerModal.tsx b/src/pages/setting/components/CreateBannerModal.tsx new file mode 100644 index 0000000..4acee61 --- /dev/null +++ b/src/pages/setting/components/CreateBannerModal.tsx @@ -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 = ({ isOpen, onClose }) => { + const [formData, setFormData] = useState({ + imageUrl: '', + altText: '', + title: '', + linkUrl: '', + isActive: true, + order: 1, + }) + + const [imageFile, setImageFile] = useState([]) + + 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 ( + +
+ {/* تصویر بنر */} +
+ setImageFile(files)} + /> +
+ + {/* عنوان */} +
+ setFormData(prev => ({ ...prev, title: e.target.value }))} + placeholder="عنوان بنر را وارد کنید" + required + /> +
+ + {/* متن جایگزین */} +
+ setFormData(prev => ({ ...prev, altText: e.target.value }))} + placeholder="توضیح تصویر برای دسترسی" + required + /> +
+ + {/* لینک */} +
+ setFormData(prev => ({ ...prev, linkUrl: e.target.value }))} + placeholder="https://example.com" + required + /> +
+ + {/* ترتیب نمایش */} +
+ setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 1 }))} + placeholder="1" + min={1} + required + /> +
+ + {/* وضعیت */} +
+ setFormData(prev => ({ ...prev, isActive: value }))} + label="فعال" + /> +
+ +
+ + +
+
+
+ ) +} + +export default CreateBannerModal diff --git a/src/pages/setting/components/EditBannerModal.tsx b/src/pages/setting/components/EditBannerModal.tsx new file mode 100644 index 0000000..e6b41a5 --- /dev/null +++ b/src/pages/setting/components/EditBannerModal.tsx @@ -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 = ({ banner, isOpen, onClose, onSuccess }) => { + const [formData, setFormData] = useState({ + imageUrl: '', + altText: '', + title: '', + linkUrl: '', + isActive: true, + order: 1, + }) + + const [imageFile, setImageFile] = useState([]) + + 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 ( + +
+ {/* تصویر بنر */} +
+
+ {formData.altText} { + const target = e.target as HTMLImageElement + target.src = '/placeholder-image.png' + }} + /> +
+ setImageFile(files)} + /> +
+ + {/* عنوان */} +
+ setFormData(prev => ({ ...prev, title: e.target.value }))} + placeholder="عنوان بنر را وارد کنید" + required + /> +
+ + {/* متن جایگزین */} +
+ setFormData(prev => ({ ...prev, altText: e.target.value }))} + placeholder="توضیح تصویر برای دسترسی" + required + /> +
+ + {/* لینک */} +
+ setFormData(prev => ({ ...prev, linkUrl: e.target.value }))} + placeholder="https://example.com" + required + /> +
+ + {/* ترتیب نمایش */} +
+ setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 1 }))} + placeholder="1" + min={1} + required + /> +
+ + {/* وضعیت */} +
+ setFormData(prev => ({ ...prev, isActive: value }))} + label="فعال" + /> +
+ +
+ + +
+
+
+ ) +} + +export default EditBannerModal diff --git a/src/pages/setting/hooks/useSettingData.ts b/src/pages/setting/hooks/useSettingData.ts index d2052e7..02dead9 100644 --- a/src/pages/setting/hooks/useSettingData.ts +++ b/src/pages/setting/hooks/useSettingData.ts @@ -1,7 +1,7 @@ import * as api from "../service/SettingService"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 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 = () => { return useQuery({ @@ -144,4 +144,38 @@ export const useUpdateShop = () => { queryClient.invalidateQueries({ queryKey: ["shop"] }); }, }); -}; \ No newline at end of file +}; + +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"] }); + }, + }); +}; + diff --git a/src/pages/setting/service/SettingService.ts b/src/pages/setting/service/SettingService.ts index d06d4bf..d38c370 100644 --- a/src/pages/setting/service/SettingService.ts +++ b/src/pages/setting/service/SettingService.ts @@ -1,6 +1,7 @@ import axios from "../../../config/axios"; import type { AboutUsResponse, + BannersResponse, CreateAboutUs, CreateDocumentType, CreateFaqType, @@ -10,6 +11,7 @@ import type { FaqResponse, PricingResponse, ShopResponse, + UpdateBannerType, UpdateShopType, UpdateSiteSetting, } from "../types/Types"; @@ -115,3 +117,23 @@ export const updateShop = async (params: UpdateShopType) => { const { data } = await axios.patch(`/admin/shop`, params); return data; }; + +export const getBanners = async (): Promise => { + 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; +}; diff --git a/src/pages/setting/types/Types.ts b/src/pages/setting/types/Types.ts index d97812a..ea590ea 100644 --- a/src/pages/setting/types/Types.ts +++ b/src/pages/setting/types/Types.ts @@ -201,3 +201,43 @@ export interface UpdateShopType { shopPostalCode: 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; +} diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 1d6bed0..87a73b9 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -56,6 +56,7 @@ import JobsPage from '@/pages/setting/JobsPage' import ManageDocument from '@/pages/setting/ManageDocument' import Pricing from '@/pages/setting/Pricing' import Shop from '@/pages/setting/Shop' +import Banners from '@/pages/setting/Banners' const MainRouter: FC = () => { @@ -137,6 +138,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } />