diff --git a/src/components/DefaultTableSkeleton.tsx b/src/components/DefaultTableSkeleton.tsx index 867f975..962d9dc 100644 --- a/src/components/DefaultTableSkeleton.tsx +++ b/src/components/DefaultTableSkeleton.tsx @@ -1,5 +1,4 @@ import { type FC, Fragment } from 'react' -import Td from './Td' import Skeleton from 'react-loading-skeleton' type Props = { @@ -15,9 +14,9 @@ const DefaultTableSkeleton: FC = ({ tdCount, trCount = 5 }) => { { Array.from({ length: tdCount }).map((_, colIndex) => ( - - - + + + )) } diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 6f82515..ea4f166 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useState, memo } from 'react'; +import React, { useState, memo } from 'react'; import DefaultTableSkeleton from '../components/DefaultTableSkeleton'; import Td from '../components/Td'; import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes'; @@ -147,9 +147,7 @@ const Table = ({ const renderTableBody = () => { if (isLoading) { return ( - - - + ); } @@ -321,43 +319,4 @@ const Table = ({ ); }; -export default memo(Table) as (props: TableProps) => React.ReactElement; - -/* -مثال استفاده با pagination: - -import Table from '@/components/Table'; -import { useState } from 'react'; - -const MyComponent = () => { - const [currentPage, setCurrentPage] = useState(1); - const totalPages = 10; - - const columns = [ - { title: 'نام', key: 'name' }, - { title: 'ایمیل', key: 'email' }, - ]; - - const data = [ - { id: 1, name: 'علی', email: 'ali@example.com' }, - { id: 2, name: 'فاطمه', email: 'fateme@example.com' }, - ]; - - const handlePageChange = (page: number) => { - setCurrentPage(page); - // اینجا می‌توانید API call جدید برای دریافت داده‌های صفحه جدید بزنید - }; - - return ( - - ); -}; -*/ \ No newline at end of file +export default memo(Table) as (props: TableProps) => React.ReactElement; \ No newline at end of file diff --git a/src/components/UploadBoxDraggble.tsx b/src/components/UploadBoxDraggble.tsx index 3b6bac4..00cd240 100644 --- a/src/components/UploadBoxDraggble.tsx +++ b/src/components/UploadBoxDraggble.tsx @@ -12,7 +12,8 @@ type Props = { preview?: string[], onChangePreview?: (preview: string[]) => void, getCover?: (url: string) => void, - coverUrl?: string + coverUrl?: string, + imageSize?: 'small' | 'medium' | 'large' } const UploadBoxDraggble: FC = (props: Props) => { @@ -92,9 +93,17 @@ const UploadBoxDraggble: FC = (props: Props) => { { files.length > 0 || perviews ? ( -
+
{ perviews && perviews.map((item, index) => { + const imageSizeClass = props.imageSize === 'large' + ? 'w-full h-32 rounded-lg' + : props.imageSize === 'medium' + ? 'size-20 rounded-lg' + : 'size-10 rounded-full' return (
@@ -105,10 +114,11 @@ const UploadBoxDraggble: FC = (props: Props) => { } }} src={item} className={clx( - 'size-10 rounded-full object-cover', + imageSizeClass, + 'object-cover cursor-pointer', cover === item ? 'border-2 border-red-400' : '' )} /> -
handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'> +
handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center cursor-pointer z-10'>
@@ -123,24 +133,30 @@ const UploadBoxDraggble: FC = (props: Props) => {
{file.name}
-
+
handleDelete(index)} className='size-4 ' color='red' />
) } - else + else { + const imageSizeClass = props.imageSize === 'large' + ? 'w-full h-32 rounded-lg' + : props.imageSize === 'medium' + ? 'size-20 rounded-lg' + : 'size-10 rounded-full' return (
- {file.name} -
+ {file.name} +
handleDelete(index)} className='size-4 ' color='red' />
) + } }) }
diff --git a/src/config/func.ts b/src/config/func.ts index 5b3e32a..b7fe5bc 100644 --- a/src/config/func.ts +++ b/src/config/func.ts @@ -52,3 +52,23 @@ export const timeAgo = (date: string | Date): string => { return "همین الان"; }; + +export interface IGeneralError { + status: number; + success: boolean; + error: { + message: string[]; + }; +} + +export const extractErrorMessage = ( + error: Error | unknown, + fallbackMessage: string = "خطایی رخ داده است" +): string => { + try { + const axiosError = error as { response?: { data: IGeneralError } }; + return axiosError?.response?.data?.error?.message?.[0] || fallbackMessage; + } catch { + return fallbackMessage; + } +}; diff --git a/src/index.css b/src/index.css index f49305c..bc9871f 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "react-loading-skeleton/dist/skeleton.css"; html { direction: rtl; diff --git a/src/pages/food/Create.tsx b/src/pages/food/Create.tsx index 1007347..be12df5 100644 --- a/src/pages/food/Create.tsx +++ b/src/pages/food/Create.tsx @@ -75,7 +75,8 @@ const CreateFood: FC = () => { const foodData: CreateFoodType = { ...values, isActive, - images: imageUrls + images: imageUrls, + content: values.content.filter(item => item && item.trim().length > 0) } createFood(foodData, { diff --git a/src/pages/food/Update.tsx b/src/pages/food/Update.tsx new file mode 100644 index 0000000..bc4c06c --- /dev/null +++ b/src/pages/food/Update.tsx @@ -0,0 +1,219 @@ +import { type FC, useState, useEffect } from 'react' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { TickCircle } from 'iconsax-react' +import { toast } from 'react-toastify' +import { useParams } from 'react-router-dom' +import Button from '@/components/Button' +import CreateFoodSidebar from './components/CreateFoodSidebar' +import BasicInfoSection from './components/BasicInfoSection' +import FoodContentSection from './components/FoodContentSection' +import MealTimesSection from './components/MealTimesSection' +import WeekDaysSection from './components/WeekDaysSection' +import ServiceOptionsSection from './components/ServiceOptionsSection' +import type { CreateFoodType } from './types/Types' +import { useUpdateFood, useGetFoodDetails, useGetCategories } from './hooks/useFoodData' +import { useMultipleUpload } from '../uploader/hooks/useUploaderData' +import { Pages } from '@/config/Pages' +import type { ErrorType } from '@/helpers/types' +import { extractErrorMessage } from '@/config/func' + +const UpdateFood: FC = () => { + const { id } = useParams<{ id: string }>() + const { data: foodData, isLoading } = useGetFoodDetails(id || '') + const { data: categoriesData } = useGetCategories() + const [isActive, setIsActive] = useState(true) + const [isSpecial, setIsSpecial] = useState(false) + const [imageFiles, setImageFiles] = useState([]) + const [existingImages, setExistingImages] = useState([]) + const { mutate: updateFood } = useUpdateFood() + const { mutate: multipleUpload } = useMultipleUpload() + + const categories = categoriesData?.data?.map(category => ({ + label: category.title, + value: category.id + })) || [] + + const food = foodData?.data + + const formik = useFormik({ + initialValues: { + title: '', + desc: '', + content: [], + categoryIds: [], + price: 0, + discount: 0, + points: 0, + prepareTime: 0, + stock: 0, + stockDefault: 0, + breakfast: false, + noon: false, + dinner: false, + mon: false, + tue: false, + wed: false, + thu: false, + fri: false, + sat: false, + sun: false, + pickupServe: false, + inPlaceServe: false, + isActive: true, + images: [] + }, + validationSchema: Yup.object().shape({ + title: Yup.string().required('نام غذا الزامی است'), + desc: Yup.string().required('توضیحات غذا الزامی است'), + categoryIds: Yup.array().min(1, 'حداقل یک دسته‌بندی الزامی است'), + price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'), + prepareTime: Yup.number().required('زمان آماده‌سازی الزامی است').min(0, 'زمان آماده‌سازی باید مثبت باشد'), + stock: Yup.number().required('موجودی الزامی است').min(0, 'موجودی باید مثبت باشد'), + stockDefault: Yup.number().required('موجودی پیش‌فرض الزامی است').min(0, 'موجودی پیش‌فرض باید مثبت باشد'), + points: Yup.number().min(0, 'امتیاز باید مثبت باشد'), + discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'), + content: Yup.array().of(Yup.string()) + }), + onSubmit: (values) => { + if (!id) { + toast.error('شناسه غذا یافت نشد') + return + } + + const submitFood = (newImageUrls: string[] = []) => { + const allImages = [...values.images, ...newImageUrls] + const foodData: CreateFoodType = { + ...values, + isActive, + images: allImages, + content: values.content.filter(item => item && item.trim().length > 0) + } + + updateFood({ id, params: foodData }, { + onSuccess: () => { + toast.success('غذا با موفقیت به‌روزرسانی شد') + window.location.href = Pages.foods.list + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)) + } + }) + } + + if (imageFiles.length > 0) { + multipleUpload(imageFiles, { + onSuccess: (response) => { + const imageUrls = response?.data?.map(item => item.url) || [] + submitFood(imageUrls) + }, + onError: (error: ErrorType) => { + toast.error(error?.response?.data?.error?.message[0] || 'خطا در آپلود تصاویر') + } + }) + } else { + submitFood() + } + } + }) + + useEffect(() => { + if (food) { + formik.setValues({ + title: food.title || '', + desc: food.desc || '', + content: food.content || [], + categoryIds: food.categories?.map(cat => cat.id) || [], + price: food.price || 0, + discount: food.discount || 0, + points: food.points || 0, + prepareTime: food.prepareTime || 0, + stock: food.stock || 0, + stockDefault: food.stockDefault || 0, + breakfast: food.breakfast || false, + noon: food.noon || false, + dinner: food.dinner || false, + mon: food.mon || false, + tue: food.tue || false, + wed: food.wed || false, + thu: food.thu || false, + fri: food.fri || false, + sat: food.sat || false, + sun: food.sun || false, + pickupServe: food.pickupServe || false, + inPlaceServe: food.inPlaceServe || false, + isActive: food.isActive || false, + images: food.images || [] + }) + setIsActive(food.isActive || false) + setExistingImages(food.images || []) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [food]) + + useEffect(() => { + formik.setFieldValue('images', existingImages) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [existingImages]) + + if (isLoading) { + return ( +
+
در حال بارگذاری...
+
+ ) + } + + if (!food) { + return ( +
+
غذا یافت نشد
+
+ ) + } + + return ( +
+
+
+ ویرایش غذا +
+
+ +
+
+ +
+
+
+ + + + + +
+
+ + +
+
+ ) +} + +export default UpdateFood diff --git a/src/pages/food/components/BasicInfoSection.tsx b/src/pages/food/components/BasicInfoSection.tsx new file mode 100644 index 0000000..59adbe9 --- /dev/null +++ b/src/pages/food/components/BasicInfoSection.tsx @@ -0,0 +1,121 @@ +import { type FC } from 'react' +import type { FormikProps } from 'formik' +import Input from '@/components/Input' +import Select from '@/components/Select' +import Textarea from '@/components/Textarea' +import type { CreateFoodType } from '../types/Types' + +type BasicInfoSectionProps = { + formik: FormikProps + categories: Array<{ label: string; value: string }> +} + +const BasicInfoSection: FC = ({ formik, categories }) => { + return ( + <> + + +
+ formik.setFieldValue('price', Number(e.target.value))} + error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''} + /> + + formik.setFieldValue('discount', Number(e.target.value))} + error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''} + /> + + formik.setFieldValue('points', Number(e.target.value))} + error_text={formik.touched.points && formik.errors.points ? formik.errors.points : ''} + /> +
+ +
+ formik.setFieldValue('prepareTime', Number(e.target.value))} + error_text={formik.touched.prepareTime && formik.errors.prepareTime ? formik.errors.prepareTime : ''} + /> + + formik.setFieldValue('stock', Number(e.target.value))} + error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''} + /> + + formik.setFieldValue('stockDefault', Number(e.target.value))} + error_text={formik.touched.stockDefault && formik.errors.stockDefault ? formik.errors.stockDefault : ''} + /> +
+ +
+