diff --git a/frontend/src/components/progress-bar.tsx b/frontend/src/components/progress-bar.tsx new file mode 100644 index 0000000..55fc26c --- /dev/null +++ b/frontend/src/components/progress-bar.tsx @@ -0,0 +1,34 @@ +import type { CSSProperties } from 'react'; + +export default function ProgressBar({ progress }: { progress: number }) { + const containerStyles: CSSProperties = { + height: 20, + width: '100%', + backgroundColor: '#e0e0de', + borderRadius: 50, + marginTop: 10, + }; + + const fillerStyles: CSSProperties = { + height: '100%', + width: `${progress}%`, + backgroundColor: 'var(--primary)', + borderRadius: 'inherit', + textAlign: 'right', + transition: 'width 0.2s ease-in-out', + }; + + const labelStyles: CSSProperties = { + padding: 5, + color: 'white', + fontWeight: 'bold', + }; + + return ( +
+
+ {`${progress}%`} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/upload-component.tsx b/frontend/src/components/upload-component.tsx index a10c665..6f19260 100644 --- a/frontend/src/components/upload-component.tsx +++ b/frontend/src/components/upload-component.tsx @@ -1,12 +1,13 @@ 'use client' -import {useState} from 'react' -import {Cloud} from 'react-feather' -import {useDropzone} from 'react-dropzone' -import {toast} from 'sonner' +import { useState } from 'react' +import { useDropzone } from 'react-dropzone' +import { Cloud } from 'react-feather' +import { toast } from 'sonner' -import {Button} from '@/components/ui/button' -import {uploadDocuments} from '@/lib/documents' +import { Button } from '@/components/ui/button' +import { uploadDocuments } from '@/lib/documents' +import ProgressBar from './progress-bar' const ACCEPTED_FILE_TYPES = { 'application/pdf': ['.pdf'], @@ -22,12 +23,35 @@ export default function UploadComponent({ callback?: () => void }) { const [isUploading, setIsUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0); + const {getRootProps, getInputProps, isDragActive} = useDropzone({ accept: ACCEPTED_FILE_TYPES, maxSize: MAX_FILE_SIZE, onDrop: async (documents) => { setIsUploading(true) - await uploadDocuments(courseId, documents) + await uploadDocuments(courseId, documents, (progressEvent) => { + if (progressEvent.total) { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total, + ); + setUploadProgress(percentCompleted); + } + }) + .then((result) => { + if (result.ok) { + toast.success('Files uploaded successfully') + } else { + toast.error(result.error?.message || 'Failed to upload files') + } + }) + .catch(() => { + toast.error('Failed to upload files') + }) + .finally(async () => { + setUploadProgress(0); // Reset progress after upload completes + setIsUploading(false) + }) if (callback) { await callback() } @@ -69,7 +93,12 @@ export default function UploadComponent({ > -
+ {isUploading ? ( +
+

Uploading...

+ +
+ ) : (

{isDragActive ? 'Drop files here' : 'Drag and drop files here'}

@@ -87,7 +116,7 @@ export default function UploadComponent({ > {isUploading ? 'Uploading...' : 'Browse Files'} -
+
)} ) } diff --git a/frontend/src/lib/documents.ts b/frontend/src/lib/documents.ts index e78da6f..516d079 100644 --- a/frontend/src/lib/documents.ts +++ b/frontend/src/lib/documents.ts @@ -1,11 +1,13 @@ -import {DocumentsService} from '@/client' +import { DocumentsService } from '@/client' -import {Result} from './result' -import {mapApiError} from './mapApiError' +import { AxiosProgressEvent } from 'axios' +import { mapApiError } from './mapApiError' +import { Result } from './result' export async function uploadDocuments( courseId: string, documents: File[], + onUploadProgress?: (progressEvent: AxiosProgressEvent) => void, ): Promise> { try { const formData = new FormData() @@ -22,6 +24,7 @@ export async function uploadDocuments( // openapi-ts misinterpret this as a plain string rather than a File // object. This bypasses the validation requestValidator: async () => {}, + onUploadProgress: onUploadProgress, }) return {