Spaces:
Sleeping
Sleeping
| import React, { useRef, useState } from 'react'; | |
| import { Button } from './ui/button'; | |
| import { Upload, File, X, FileText, FileSpreadsheet, Presentation } from 'lucide-react'; | |
| import { Card } from './ui/card'; | |
| import { Badge } from './ui/badge'; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; | |
| import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'; | |
| import type { UploadedFile, FileType } from '../App'; | |
| interface FileUploadAreaProps { | |
| uploadedFiles: UploadedFile[]; | |
| onFileUpload: (files: File[]) => void; | |
| onRemoveFile: (index: number) => void; | |
| onFileTypeChange: (index: number, type: FileType) => void; | |
| disabled?: boolean; | |
| } | |
| interface PendingFile { | |
| file: File; | |
| type: FileType; | |
| } | |
| export function FileUploadArea({ | |
| uploadedFiles, | |
| onFileUpload, | |
| onRemoveFile, | |
| onFileTypeChange, | |
| disabled = false, | |
| }: FileUploadAreaProps) { | |
| const [isDragging, setIsDragging] = useState(false); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]); | |
| const [showTypeDialog, setShowTypeDialog] = useState(false); | |
| const handleDragOver = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| if (!disabled) setIsDragging(true); | |
| }; | |
| const handleDragLeave = () => { | |
| setIsDragging(false); | |
| }; | |
| const handleDrop = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setIsDragging(false); | |
| if (disabled) return; | |
| const files = Array.from(e.dataTransfer.files).filter((file) => | |
| ['.pdf', '.docx', '.pptx'].some((ext) => file.name.toLowerCase().endsWith(ext)) | |
| ); | |
| if (files.length > 0) { | |
| setPendingFiles(files.map(file => ({ file, type: 'other' as FileType }))); | |
| setShowTypeDialog(true); | |
| } | |
| }; | |
| const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const files = Array.from(e.target.files || []); | |
| if (files.length > 0) { | |
| setPendingFiles(files.map(file => ({ file, type: 'other' as FileType }))); | |
| setShowTypeDialog(true); | |
| } | |
| e.target.value = ''; | |
| }; | |
| const handleConfirmUpload = () => { | |
| onFileUpload(pendingFiles.map(pf => pf.file)); | |
| // Update the parent's file types | |
| const startIndex = uploadedFiles.length; | |
| pendingFiles.forEach((pf, idx) => { | |
| setTimeout(() => { | |
| onFileTypeChange(startIndex + idx, pf.type); | |
| }, 0); | |
| }); | |
| setPendingFiles([]); | |
| setShowTypeDialog(false); | |
| }; | |
| const handleCancelUpload = () => { | |
| setPendingFiles([]); | |
| setShowTypeDialog(false); | |
| }; | |
| const handlePendingFileTypeChange = (index: number, type: FileType) => { | |
| setPendingFiles(prev => prev.map((pf, i) => | |
| i === index ? { ...pf, type } : pf | |
| )); | |
| }; | |
| const getFileIcon = (filename: string) => { | |
| if (filename.endsWith('.pdf')) return FileText; | |
| if (filename.endsWith('.docx')) return File; | |
| if (filename.endsWith('.pptx')) return Presentation; | |
| return File; | |
| }; | |
| const formatFileSize = (bytes: number) => { | |
| if (bytes < 1024) return bytes + ' B'; | |
| if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; | |
| return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; | |
| }; | |
| const getFileTypeLabel = (type: FileType) => { | |
| const labels: Record<FileType, string> = { | |
| 'syllabus': 'Syllabus', | |
| 'lecture-slides': 'Lecture Slides / PPT', | |
| 'literature-review': 'Literature Review / Paper', | |
| 'other': 'Other Course Document', | |
| }; | |
| return labels[type]; | |
| }; | |
| return ( | |
| <Card className="p-4 space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <h4 className="text-sm">Course Materials</h4> | |
| {uploadedFiles.length > 0 && ( | |
| <Badge variant="secondary">{uploadedFiles.length} file(s)</Badge> | |
| )} | |
| </div> | |
| {/* Upload Area */} | |
| <div | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| className={` | |
| border-2 border-dashed rounded-lg p-4 text-center transition-colors | |
| ${isDragging ? 'border-primary bg-accent' : 'border-border'} | |
| ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} | |
| `} | |
| onClick={() => !disabled && fileInputRef.current?.click()} | |
| > | |
| <Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" /> | |
| <p className="text-sm text-muted-foreground mb-1"> | |
| {disabled ? 'Please log in to upload' : 'Drop files or click to upload'} | |
| </p> | |
| <p className="text-xs text-muted-foreground"> | |
| .pdf, .docx, .pptx | |
| </p> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| multiple | |
| accept=".pdf,.docx,.pptx" | |
| onChange={handleFileSelect} | |
| className="hidden" | |
| disabled={disabled} | |
| /> | |
| </div> | |
| {/* Uploaded Files List */} | |
| {uploadedFiles.length > 0 && ( | |
| <div className="space-y-3 max-h-64 overflow-y-auto"> | |
| {uploadedFiles.map((uploadedFile, index) => { | |
| const Icon = getFileIcon(uploadedFile.file.name); | |
| return ( | |
| <div | |
| key={index} | |
| className="p-3 bg-muted rounded-md space-y-2" | |
| > | |
| <div className="flex items-center gap-2 group"> | |
| <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" /> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm truncate">{uploadedFile.file.name}</p> | |
| <p className="text-xs text-muted-foreground"> | |
| {formatFileSize(uploadedFile.file.size)} | |
| </p> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| onRemoveFile(index); | |
| }} | |
| > | |
| <X className="h-3 w-3" /> | |
| </Button> | |
| </div> | |
| <div className="space-y-1"> | |
| <label className="text-xs text-muted-foreground">File Type</label> | |
| <Select | |
| value={uploadedFile.type} | |
| onValueChange={(value) => onFileTypeChange(index, value as FileType)} | |
| > | |
| <SelectTrigger className="h-8 text-xs"> | |
| <SelectValue /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="syllabus">Syllabus</SelectItem> | |
| <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem> | |
| <SelectItem value="literature-review">Literature Review / Paper</SelectItem> | |
| <SelectItem value="other">Other Course Document</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| {/* Type Selection Dialog */} | |
| {showTypeDialog && ( | |
| <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}> | |
| <DialogContent className="sm:max-w-[425px]"> | |
| <DialogHeader> | |
| <DialogTitle>Select File Types</DialogTitle> | |
| <DialogDescription> | |
| Please select the type for each file you are uploading. | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="space-y-3 max-h-64 overflow-y-auto"> | |
| {pendingFiles.map((pendingFile, index) => { | |
| const Icon = getFileIcon(pendingFile.file.name); | |
| return ( | |
| <div | |
| key={index} | |
| className="p-3 bg-muted rounded-md space-y-2" | |
| > | |
| <div className="flex items-center gap-2 group"> | |
| <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" /> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm truncate">{pendingFile.file.name}</p> | |
| <p className="text-xs text-muted-foreground"> | |
| {formatFileSize(pendingFile.file.size)} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="space-y-1"> | |
| <label className="text-xs text-muted-foreground">File Type</label> | |
| <Select | |
| value={pendingFile.type} | |
| onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)} | |
| > | |
| <SelectTrigger className="h-8 text-xs"> | |
| <SelectValue /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="syllabus">Syllabus</SelectItem> | |
| <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem> | |
| <SelectItem value="literature-review">Literature Review / Paper</SelectItem> | |
| <SelectItem value="other">Other Course Document</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <DialogFooter> | |
| <Button | |
| variant="outline" | |
| onClick={handleCancelUpload} | |
| > | |
| Cancel | |
| </Button> | |
| <Button | |
| onClick={handleConfirmUpload} | |
| > | |
| Upload | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| )} | |
| </Card> | |
| ); | |
| } |