diff --git a/src/components/UploadBoxDraggble.tsx b/src/components/UploadBoxDraggble.tsx index b3f249a..9aa2d2a 100644 --- a/src/components/UploadBoxDraggble.tsx +++ b/src/components/UploadBoxDraggble.tsx @@ -2,6 +2,7 @@ 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'; +import UploadProgressBar from './UploadProgressBar'; type Props = { label: string; @@ -15,6 +16,8 @@ type Props = { /** نمایش وضعیت بارگذاری داخل باکس آپلود */ isLoading?: boolean, loadingLabel?: string, + /** درصد پیشرفت آپلود (۰ تا ۱۰۰) */ + uploadProgress?: number, /** بدون پیش‌نمایش زیر باکس (مثلاً گالری جداگانه دارد) */ hidePreview?: boolean, } @@ -77,8 +80,16 @@ const UploadBoxDraggble: FC = (props: Props) => { }, [props.coverUrl]) + const showProgressOnly = props.isLoading && props.uploadProgress !== undefined; + return (
+ {showProgressOnly ? ( + + ) : (
= (props: Props) => { )}
+ )} { !props.hidePreview && (files.length > 0 || perviews.length > 0) ? ( diff --git a/src/components/UploadProgressBar.tsx b/src/components/UploadProgressBar.tsx new file mode 100644 index 0000000..2309ffd --- /dev/null +++ b/src/components/UploadProgressBar.tsx @@ -0,0 +1,26 @@ +import { type FC } from "react"; + +type UploadProgressBarProps = { + progress: number; + label?: string; +}; + +const UploadProgressBar: FC = ({ progress, label }) => { + const clampedProgress = Math.min(100, Math.max(0, progress)); + + return ( +
+
+
+
+
+ {label ?? `${clampedProgress}٪`} +
+
+ ); +}; + +export default UploadProgressBar; diff --git a/src/components/VideoUploadZone.tsx b/src/components/VideoUploadZone.tsx index 8f8eab1..6f540cb 100644 --- a/src/components/VideoUploadZone.tsx +++ b/src/components/VideoUploadZone.tsx @@ -1,12 +1,14 @@ import { type FC } from "react"; import { DocumentUpload, VideoSquare } from "iconsax-react"; import { clx } from "@/helpers/utils"; +import UploadProgressBar from "@/components/UploadProgressBar"; type VideoUploadZoneProps = { getRootProps: () => React.HTMLAttributes; getInputProps: () => React.InputHTMLAttributes; isLoading?: boolean; loadingLabel?: string; + uploadProgress?: number; }; const VideoUploadZone: FC = ({ @@ -14,7 +16,17 @@ const VideoUploadZone: FC = ({ getInputProps, isLoading = false, loadingLabel = "در حال آپلود...", + uploadProgress, }) => { + if (isLoading && uploadProgress !== undefined) { + return ( + + ); + } + return (
{ if (!file) return try { - const result = await uploadFile(file) + const result = await uploadFile({ file }) const imageUrl = result?.data?.url if (!imageUrl) return diff --git a/src/pages/editor/components/sidebar/instructions/AudioInput.tsx b/src/pages/editor/components/sidebar/instructions/AudioInput.tsx index aff8954..e93eb88 100644 --- a/src/pages/editor/components/sidebar/instructions/AudioInput.tsx +++ b/src/pages/editor/components/sidebar/instructions/AudioInput.tsx @@ -28,7 +28,7 @@ const AudioInput: FC = () => { try { for (const file of acceptedFiles) { try { - const result = await uploadFile(file); + const result = await uploadFile({ file }); const audioUrl = result?.data?.url; if (!audioUrl) continue; const audioId = `audio-${Date.now()}-${Math.random()}`; diff --git a/src/pages/editor/components/sidebar/instructions/ImageGallery.tsx b/src/pages/editor/components/sidebar/instructions/ImageGallery.tsx index 3e87717..14cd19c 100644 --- a/src/pages/editor/components/sidebar/instructions/ImageGallery.tsx +++ b/src/pages/editor/components/sidebar/instructions/ImageGallery.tsx @@ -10,14 +10,23 @@ const ImageGallery: FC = () => { const { gallery, addGalleryItem, removeGalleryItem } = useEditorStore(); const { mutateAsync: uploadFile } = useSingleUpload(); const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); const handleFileChange = async (files: File[]) => { if (files.length === 0) return; setIsUploading(true); + setUploadProgress(0); try { - for (const file of files) { - const result = await uploadFile(file); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const result = await uploadFile({ + file, + onProgress: (fileProgress) => { + const overall = Math.round(((i + fileProgress / 100) / files.length) * 100); + setUploadProgress(overall); + }, + }); const imageUrl = result?.data?.url; if (!imageUrl) continue; @@ -33,6 +42,7 @@ const ImageGallery: FC = () => { // TODO: show error toast } finally { setIsUploading(false); + setUploadProgress(0); } }; @@ -54,6 +64,7 @@ const ImageGallery: FC = () => { isMultiple={true} hidePreview isLoading={isUploading} + uploadProgress={isUploading ? uploadProgress : undefined} /> {gallery.length === 0 && !isUploading && ( diff --git a/src/pages/editor/components/sidebar/instructions/VideoInput.tsx b/src/pages/editor/components/sidebar/instructions/VideoInput.tsx index 766d932..27759be 100644 --- a/src/pages/editor/components/sidebar/instructions/VideoInput.tsx +++ b/src/pages/editor/components/sidebar/instructions/VideoInput.tsx @@ -10,16 +10,27 @@ const VideoInput: FC = () => { useEditorStore(); const { mutateAsync: uploadFile } = useSingleUpload(); const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); const onDrop = useCallback( async (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return; setIsUploading(true); + setUploadProgress(0); try { - for (const file of acceptedFiles) { + for (let i = 0; i < acceptedFiles.length; i++) { + const file = acceptedFiles[i]; try { - const result = await uploadFile(file); + const result = await uploadFile({ + file, + onProgress: (fileProgress) => { + const overall = Math.round( + ((i + fileProgress / 100) / acceptedFiles.length) * 100 + ); + setUploadProgress(overall); + }, + }); const videoUrl = result?.data?.url; if (!videoUrl) continue; const videoId = `video-${Date.now()}-${Math.random()}`; @@ -41,6 +52,7 @@ const VideoInput: FC = () => { } } finally { setIsUploading(false); + setUploadProgress(0); } }, [addObject, setSelectedObjectId, setTool, uploadFile] @@ -71,6 +83,7 @@ const VideoInput: FC = () => { getRootProps={getRootProps} getInputProps={getInputProps} isLoading={isUploading} + uploadProgress={isUploading ? uploadProgress : undefined} /> {videoObjects.length > 0 && ( diff --git a/src/pages/uploader/hooks/useUploaderData.ts b/src/pages/uploader/hooks/useUploaderData.ts index 04e5cbd..30f3c98 100644 --- a/src/pages/uploader/hooks/useUploaderData.ts +++ b/src/pages/uploader/hooks/useUploaderData.ts @@ -1,9 +1,15 @@ import { useMutation } from "@tanstack/react-query"; import * as api from "../service/UploaderService"; +export type SingleUploadParams = { + file: File; + onProgress?: (progress: number) => void; +}; + export const useSingleUpload = () => { return useMutation({ - mutationFn: api.singleUpload, + mutationFn: ({ file, onProgress }: SingleUploadParams) => + api.singleUpload(file, onProgress), }); }; diff --git a/src/pages/uploader/service/UploaderService.ts b/src/pages/uploader/service/UploaderService.ts index 299478f..409d465 100644 --- a/src/pages/uploader/service/UploaderService.ts +++ b/src/pages/uploader/service/UploaderService.ts @@ -5,13 +5,21 @@ import type { } from "../types/Types"; export const singleUpload = async ( - file: File + file: File, + onProgress?: (progress: number) => void ): Promise => { const formData = new FormData(); formData.append("file", file); const { data } = await axios.post( `/admin/single-file`, - formData + formData, + { + onUploadProgress: (event) => { + if (event.total) { + onProgress?.(Math.round((event.loaded / event.total) * 100)); + } + }, + } ); return data; };