From 5acfad3e2e79d3eb82aa15f132743cd1ef2e3fde Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 1 Nov 2025 11:55:02 +0330 Subject: [PATCH] learning --- src/components/UploadBoxDraggble.tsx | 156 ++++++++++++ src/config/Paths.tsx | 7 +- src/pages/learning/Category.tsx | 75 ++++++ src/pages/learning/Create.tsx | 193 +++++++++++++++ src/pages/learning/List.tsx | 89 +++++++ .../learning/components/CreateCategory.tsx | 92 +++++++ .../components/CreateLearningSidebar.tsx | 40 +++ src/pages/learning/hooks/useLearningData.ts | 229 ++++++++++++++++++ src/pages/learning/service/LearningService.ts | 25 ++ src/pages/learning/types/LearningTypes.ts | 34 +++ src/router/MainRouter.tsx | 7 + src/shared/Sidebar.tsx | 2 +- 12 files changed, 947 insertions(+), 2 deletions(-) create mode 100644 src/components/UploadBoxDraggble.tsx create mode 100644 src/pages/learning/Category.tsx create mode 100644 src/pages/learning/Create.tsx create mode 100644 src/pages/learning/List.tsx create mode 100644 src/pages/learning/components/CreateCategory.tsx create mode 100644 src/pages/learning/components/CreateLearningSidebar.tsx create mode 100644 src/pages/learning/hooks/useLearningData.ts create mode 100644 src/pages/learning/service/LearningService.ts create mode 100644 src/pages/learning/types/LearningTypes.ts diff --git a/src/components/UploadBoxDraggble.tsx b/src/components/UploadBoxDraggble.tsx new file mode 100644 index 0000000..e0a48aa --- /dev/null +++ b/src/components/UploadBoxDraggble.tsx @@ -0,0 +1,156 @@ +import { CloseCircle, DocumentUpload, Gallery } from 'iconsax-react' +import { type FC, useCallback, useEffect, useState } from 'react' +import { useDropzone } from 'react-dropzone'; +import { clx } from '../helpers/utils'; + +type Props = { + label: string; + onChange: (file: File[]) => void; + isMultiple?: boolean; + isFile?: boolean, + preview?: string[], + onChangePreview?: (preview: string[]) => void, + getCover?: (url: string) => void, + coverUrl?: string, + accept?: string +} + +const UploadBoxDraggble: FC = (props: Props) => { + + const [files, setFiles] = useState([]) + const [perviews, setPerviews] = useState(props.preview ? props.preview : []) + const [isFill, setIsFill] = useState(false) + const [cover, setCover] = useState(props.coverUrl ? props.coverUrl : '') + + const onDrop = useCallback((acceptedFiles: File[]) => { + if (props.isMultiple) { + const array = [...files] + array.push(acceptedFiles[0]) + setFiles(array) + props.onChange(array) + } else { + setFiles([acceptedFiles[0]]) + props.onChange([acceptedFiles[0]]) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [files]) + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: props.accept ? { [props.accept]: [] } : undefined + }) + + const handleDelete = (index: number) => { + const array = [...files] + array.splice(index, 1) + setFiles(array) + props.onChange(array) + } + + const handleDeletePreview = (index: number) => { + const array = [...perviews]; + array.splice(index, 1); + setPerviews(array); + if (props.onChangePreview) { + props.onChangePreview(array); + } + } + + useEffect(() => { + + if (props.preview && props.preview.length > 0 && !isFill) { + setPerviews(props.preview) + console.log('preview', props.preview); + + setIsFill(true) + } + + }, [props.preview, isFill]) + + useEffect(() => { + setCover(props.coverUrl ? props.coverUrl : '') + }, [props.coverUrl]) + + + return ( +
+
+ + +
+ {props.label} +
+ +
+ +
آپلود
+
+
+ + { + files.length > 0 || perviews ? ( +
+ { + perviews && perviews.map((item, index) => { + return ( +
+
+ { + if (props.getCover) { + setCover(item) + props.getCover(item) + } + }} src={item} + className={clx( + 'size-10 rounded-full object-cover', + 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'> + +
+
+
+ ) + }) + } + { + files.map((file, index) => { + if (props.isFile) { + return ( +
+
+
{file.name}
+
+ handleDelete(index)} className='size-4 ' color='red' /> +
+
+
+ ) + } + else + return ( +
+
+ {file.name} +
+ handleDelete(index)} className='size-4 ' color='red' /> +
+
+
+ ) + }) + } +
+ ) : null + } + +
+ ) +} + +export default UploadBoxDraggble \ No newline at end of file diff --git a/src/config/Paths.tsx b/src/config/Paths.tsx index 422b0ee..e2527cd 100644 --- a/src/config/Paths.tsx +++ b/src/config/Paths.tsx @@ -27,7 +27,6 @@ export const Paths = { tickets: { list: '/tickets', }, - learning: '/learning', auth: { login: '/auth/login', }, @@ -46,4 +45,10 @@ export const Paths = { list: "/criticisms", detail: "/criticisms/detail/", }, + learning: { + list: "/learning/list", + detail: "/learning/detail/", + create: "/learning/create", + category: "/learning/category", + }, } \ No newline at end of file diff --git a/src/pages/learning/Category.tsx b/src/pages/learning/Category.tsx new file mode 100644 index 0000000..1eee25b --- /dev/null +++ b/src/pages/learning/Category.tsx @@ -0,0 +1,75 @@ +import { type FC, useState } from 'react' +// import { useTranslation } from 'react-i18next' +import Td from '../../components/Td' +import DefaultTableSkeleton from '../../components/DefaultTableSkeleton' +import Pagination from '../../components/Pagination' +import { useGetCategory } from './hooks/useLearningData' +import { type CategoryLearningType } from './hooks/useLearningData' +import CreateCategory from './components/CreateCategory' + + +const LearningCategory: FC = () => { + + // const { t } = useTranslation('global') + const t = (key: string) => key; // Mock translation function + const [page, setPage] = useState(1) + const getCategory = useGetCategory() + + return ( +
+
+ دسته‌بندی آموزش‌ها +
+ +
+
+ +
+ { + getCategory.isPending ? +
+ +
+ : +
+ + + + + + + { + getCategory.data?.data?.categories?.map((item: CategoryLearningType) => { + return ( + + + ) + }) + } + +
+ +
+ +
+
+ +
+
+ } + + +
+
+ + +
+
+ ) +} + +export default LearningCategory \ No newline at end of file diff --git a/src/pages/learning/Create.tsx b/src/pages/learning/Create.tsx new file mode 100644 index 0000000..968a07a --- /dev/null +++ b/src/pages/learning/Create.tsx @@ -0,0 +1,193 @@ +import { type FC, useState } from 'react' +// import { useTranslation } from 'react-i18next' +import Button from '../../components/Button' +import { useFormik } from 'formik' +import { type CategoryLearningType, type CreateLearningType } from './hooks/useLearningData' +import * as Yup from 'yup' +import { useCreateLearning, useGetCategory } from './hooks/useLearningData' +// import { useSingleUpload } from '../service/hooks/useServiceData' +import { TickCircle } from 'iconsax-react' +import Input from '../../components/Input' +import Select from '../../components/Select' +import Textarea from '../../components/Textarea' +import DefaultTableSkeleton from '../../components/DefaultTableSkeleton' +import CreateLearningSidebar from './components/CreateLearningSidebar' +// import { ErrorType } from '../../helpers/types' +// Define ErrorType inline since we're using static data +type ErrorType = { + response?: { + data?: { + error: { + message: string[]; + }; + }; + }; +}; +import { toast } from 'react-toastify' +import { useNavigate } from 'react-router-dom' +import { Paths } from '../../config/Paths' + +const CreateLearning: FC = () => { + + const navigate = useNavigate() + // const { t } = useTranslation('global') + const t = (key: string) => key; // Mock translation function + // const singleUpload = useSingleUpload() + + // Mock upload hook + const singleUpload = { + mutateAsync: (formData: FormData) => { + return Promise.resolve({ + data: { + url: "/src/assets/images/new_order.png" // Mock URL + } + }); + }, + isPending: false + }; + + const createLearning = useCreateLearning() + const getCategory = useGetCategory() + const [videoFile, setVideoFile] = useState() + const [coverFile, setCoverFile] = useState() + + const formik = useFormik({ + initialValues: { + title: '', + description: '', + coverUrl: '', + videoUrl: '', + videoDuration: '', + categoryId: '' + }, + validationSchema: Yup.object().shape({ + title: Yup.string().required(t('errors.required')), + description: Yup.string().required(t('errors.required')), + videoDuration: Yup.string().required(t('errors.required')), + categoryId: Yup.string().required(t('errors.required')) + }), + onSubmit: async (values) => { + return /* + if (videoFile && coverFile) { + + const formData = new FormData() + formData.append('file', videoFile) + await singleUpload.mutateAsync(formData, { + onSuccess: (data) => { + values.videoUrl = data?.data?.url + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + + const formDataCover = new FormData() + formDataCover.append('file', coverFile) + await singleUpload.mutateAsync(formDataCover, { + onSuccess: (data) => { + values.coverUrl = data?.data?.url + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + + createLearning.mutate(values, { + onSuccess: () => { + toast.success('با موفقیت ثبت شد') + navigate(Paths.learning.list) + }, + onError: (error: ErrorType) => { + toast.error(error.response?.data?.error?.message[0]) + } + }) + } else { + toast.error('لطفا ویدیو و کاور را انتخاب کنید') + } + */ + } + }) + + return ( +
+
+
+ آموزش جدید +
+
+ +
+
+ + { + getCategory.isPending ? + + : +
+
+
+ + + +
+ +
+ +
+
توضیحات
+
+