From f69ca010b80703389fffe75fc6dca907e53df74d Mon Sep 17 00:00:00 2001
From: diogo464 <diogo464@d464.sh>
Date: Mon, 11 Aug 2025 13:40:27 +0100
Subject: basic file upload

---
 frontend/components/FileUpload.tsx | 262 +++++++++++++++++++++++++++++++++++++
 1 file changed, 262 insertions(+)
 create mode 100644 frontend/components/FileUpload.tsx

(limited to 'frontend/components/FileUpload.tsx')

diff --git a/frontend/components/FileUpload.tsx b/frontend/components/FileUpload.tsx
new file mode 100644
index 0000000..8fbb919
--- /dev/null
+++ b/frontend/components/FileUpload.tsx
@@ -0,0 +1,262 @@
+'use client'
+
+import { useState, useRef } from 'react'
+import { UPLOAD_MAX_FILES, UPLOAD_MAX_FILE_SIZE } from '@/lib/constants'
+
+// Client-side file validation function
+function validateFile(file: File): { allowed: boolean; reason?: string } {
+  if (file.size > UPLOAD_MAX_FILE_SIZE) {
+    return { allowed: false, reason: `File size exceeds ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB limit` };
+  }
+  
+  return { allowed: true };
+}
+
+interface FileUploadProps {
+  targetPath: string
+  onUploadComplete?: () => void
+}
+
+interface UploadResult {
+  filename: string
+  success: boolean
+  message: string
+}
+
+export default function FileUpload({ targetPath, onUploadComplete }: FileUploadProps) {
+  const [isDragOver, setIsDragOver] = useState(false)
+  const [isUploading, setIsUploading] = useState(false)
+  const [selectedFiles, setSelectedFiles] = useState<File[]>([])
+  const [uploadResults, setUploadResults] = useState<UploadResult[]>([])
+  const [showResults, setShowResults] = useState(false)
+  const fileInputRef = useRef<HTMLInputElement>(null)
+
+  const handleFileSelect = (files: FileList) => {
+    const fileArray = Array.from(files)
+    
+    // Validate file count
+    if (fileArray.length > UPLOAD_MAX_FILES) {
+      alert(`Too many files selected. Maximum ${UPLOAD_MAX_FILES} files allowed.`)
+      return
+    }
+
+    // Validate each file
+    const validFiles: File[] = []
+    for (const file of fileArray) {
+      const validation = validateFile(file)
+      if (!validation.allowed) {
+        alert(`File '${file.name}': ${validation.reason}`)
+        continue
+      }
+      validFiles.push(file)
+    }
+
+    setSelectedFiles(validFiles)
+    setUploadResults([])
+    setShowResults(false)
+  }
+
+  const handleDragOver = (e: React.DragEvent) => {
+    e.preventDefault()
+    setIsDragOver(true)
+  }
+
+  const handleDragLeave = (e: React.DragEvent) => {
+    e.preventDefault()
+    setIsDragOver(false)
+  }
+
+  const handleDrop = (e: React.DragEvent) => {
+    e.preventDefault()
+    setIsDragOver(false)
+    
+    if (e.dataTransfer.files) {
+      handleFileSelect(e.dataTransfer.files)
+    }
+  }
+
+  const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files) {
+      handleFileSelect(e.target.files)
+    }
+  }
+
+  const handleUpload = async () => {
+    if (selectedFiles.length === 0) return
+
+    setIsUploading(true)
+    setUploadResults([])
+
+    try {
+      const formData = new FormData()
+      selectedFiles.forEach(file => {
+        formData.append('files', file)
+      })
+      formData.append('targetPath', targetPath)
+
+      const response = await fetch('/api/upload', {
+        method: 'POST',
+        body: formData,
+      })
+
+      const result = await response.json()
+
+      if (response.ok) {
+        setUploadResults(result.results || [])
+        setShowResults(true)
+        setSelectedFiles([])
+        
+        // Clear file input
+        if (fileInputRef.current) {
+          fileInputRef.current.value = ''
+        }
+
+        // Refresh the page after successful upload
+        setTimeout(() => {
+          window.location.reload()
+        }, 1000)
+      } else {
+        alert(`Upload failed: ${result.error}`)
+      }
+    } catch (error) {
+      console.error('Upload error:', error)
+      alert('Upload failed: Network error')
+    } finally {
+      setIsUploading(false)
+    }
+  }
+
+  const removeFile = (index: number) => {
+    setSelectedFiles(prev => prev.filter((_, i) => i !== index))
+  }
+
+  const clearResults = () => {
+    setShowResults(false)
+    setUploadResults([])
+  }
+
+  return (
+    <div className="mb-6 p-4 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800">
+      <h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">Upload Files</h2>
+      
+      {/* Upload Results */}
+      {showResults && uploadResults.length > 0 && (
+        <div className="mb-4 p-3 bg-white dark:bg-gray-900 rounded border">
+          <div className="flex justify-between items-center mb-2">
+            <h3 className="font-medium text-gray-900 dark:text-gray-100">Upload Results</h3>
+            <button
+              onClick={clearResults}
+              className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
+            >
+              Clear
+            </button>
+          </div>
+          <div className="space-y-1">
+            {uploadResults.map((result, index) => (
+              <div
+                key={index}
+                className={`text-sm flex items-center gap-2 ${
+                  result.success
+                    ? 'text-green-600 dark:text-green-400'
+                    : 'text-red-600 dark:text-red-400'
+                }`}
+              >
+                <span>{result.success ? '✓' : '✗'}</span>
+                <span className="font-medium">{result.filename}:</span>
+                <span>{result.message}</span>
+              </div>
+            ))}
+          </div>
+        </div>
+      )}
+
+      {/* File Drop Zone */}
+      <div
+        className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
+          isDragOver
+            ? 'border-blue-400 bg-blue-50 dark:bg-blue-900/20'
+            : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
+        }`}
+        onDragOver={handleDragOver}
+        onDragLeave={handleDragLeave}
+        onDrop={handleDrop}
+      >
+        <div className="space-y-2">
+          <div className="text-4xl">📁</div>
+          <div className="text-gray-600 dark:text-gray-300">
+            <p className="font-medium">Drop files here or click to browse</p>
+            <p className="text-sm">
+              Maximum {UPLOAD_MAX_FILES} files, {UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB each
+            </p>
+          </div>
+          <input
+            ref={fileInputRef}
+            type="file"
+            multiple
+            className="hidden"
+            onChange={handleFileInputChange}
+          />
+          <button
+            type="button"
+            onClick={() => fileInputRef.current?.click()}
+            disabled={isUploading}
+            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400 transition-colors"
+          >
+            Browse Files
+          </button>
+        </div>
+      </div>
+
+      {/* Selected Files */}
+      {selectedFiles.length > 0 && (
+        <div className="mt-4">
+          <h3 className="font-medium mb-2 text-gray-900 dark:text-gray-100">
+            Selected Files ({selectedFiles.length})
+          </h3>
+          <div className="space-y-2">
+            {selectedFiles.map((file, index) => (
+              <div
+                key={index}
+                className="flex items-center justify-between p-2 bg-white dark:bg-gray-900 rounded border"
+              >
+                <div className="flex items-center gap-2">
+                  <span className="text-gray-400">📄</span>
+                  <div>
+                    <div className="text-sm font-medium text-gray-900 dark:text-gray-100">
+                      {file.name}
+                    </div>
+                    <div className="text-xs text-gray-500 dark:text-gray-400">
+                      {(file.size / 1024 / 1024).toFixed(2)} MB
+                    </div>
+                  </div>
+                </div>
+                <button
+                  onClick={() => removeFile(index)}
+                  disabled={isUploading}
+                  className="text-red-500 hover:text-red-700 disabled:text-gray-400 text-sm"
+                >
+                  Remove
+                </button>
+              </div>
+            ))}
+          </div>
+          
+          <button
+            onClick={handleUpload}
+            disabled={isUploading || selectedFiles.length === 0}
+            className="mt-3 px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:bg-gray-400 transition-colors flex items-center gap-2"
+          >
+            {isUploading ? (
+              <>
+                <div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
+                Uploading...
+              </>
+            ) : (
+              `Upload ${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`
+            )}
+          </button>
+        </div>
+      )}
+    </div>
+  )
+}
\ No newline at end of file
-- 
cgit 

