Spaces:
Paused
Paused
| // components/file-upload-component.js - File upload and management | |
| import { Utils } from '../utils.js'; | |
| import { StateManager } from '../services/state-manager.js'; | |
| import { ApiService } from '../services/api-service.js'; | |
| import { TranslationService } from '../services/translation-service.js'; | |
| export const FileUploadComponent = { | |
| elements: { | |
| uploadFileBtn: null, | |
| uploadFileOverlay: null, | |
| fileDropZone: null, | |
| fileInput: null, | |
| doneFileUploadBtn: null, | |
| closeFileUploadBtn: null, | |
| fileListHtml: null | |
| }, | |
| constants: { | |
| FILE_SIZE_LIMIT: 10 * 1024 * 1024, // 10 MB | |
| TOTAL_FILE_SIZE_LIMIT: 30 * 1024 * 1024, // 30 MB | |
| MAX_FILE_NAME_LENGTH: 50, | |
| ALLOWED_TYPES: ['.pdf', '.txt', '.docx', '.jpg', '.jpeg', '.png'] | |
| }, | |
| icons: { | |
| upload: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
| </svg>`, | |
| spinner: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="spinning"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> | |
| </svg>`, | |
| check: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
| </svg>`, | |
| trash: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg>` | |
| }, | |
| /** | |
| * Initialize the file upload component | |
| */ | |
| init() { | |
| this.elements.uploadFileBtn = document.getElementById('upload-file-btn'); | |
| this.elements.uploadFileOverlay = document.getElementById('upload-file-overlay'); | |
| this.elements.fileDropZone = document.getElementById('file-drop-zone'); | |
| this.elements.fileInput = document.getElementById('file-input'); | |
| this.elements.doneFileUploadBtn = document.getElementById('done-file-upload'); | |
| this.elements.closeFileUploadBtn = document.getElementById('close-file-upload-btn'); | |
| this.elements.fileListHtml = document.getElementById('file-list'); | |
| this.attachOutsideClickListener(); | |
| this.attachEventListeners(); | |
| this.renderFiles(); | |
| }, | |
| attachOutsideClickListener() { | |
| this.elements.uploadFileOverlay.addEventListener('click', (e) => { | |
| // Check if click is on the overlay itself (not its children) | |
| if (e.target === this.elements.uploadFileOverlay) { | |
| this.closeOverlay(); | |
| } | |
| }); | |
| }, | |
| /** | |
| * Attach event listeners | |
| */ | |
| attachEventListeners() { | |
| this.elements.uploadFileBtn.addEventListener('click', (e) => this.openOverlay(e)); | |
| this.elements.fileDropZone.addEventListener('click', () => this.elements.fileInput.click()); | |
| this.elements.closeFileUploadBtn.addEventListener('click', () => this.closeOverlay()); | |
| this.elements.doneFileUploadBtn.addEventListener('click', () => this.closeOverlay()); | |
| // Prevent the browser from opening a dropped file | |
| ['dragover', 'drop'].forEach(eventName => { | |
| this.elements.fileDropZone.addEventListener(eventName, (e) => e.preventDefault()); | |
| }); | |
| this.elements.fileDropZone.addEventListener('dragover', () => { | |
| this.elements.fileDropZone.classList.add('active'); | |
| }); | |
| // File drop logic | |
| this.elements.fileDropZone.addEventListener('drop', (e) => { | |
| this.elements.fileDropZone.classList.remove('active'); | |
| const addedFiles = Array.from(e.dataTransfer.files); | |
| this.handleFileAddition(addedFiles); | |
| }); | |
| // File browsing logic | |
| this.elements.fileInput.addEventListener('change', (e) => { | |
| const addedFiles = Array.from(e.target.files); | |
| this.handleFileAddition(addedFiles); | |
| }); | |
| }, | |
| /** | |
| * Open the upload overlay | |
| */ | |
| openOverlay(e) { | |
| e.preventDefault(); | |
| this.elements.uploadFileOverlay.style.display = ''; | |
| }, | |
| /** | |
| * Close the upload overlay | |
| */ | |
| closeOverlay() { | |
| this.elements.uploadFileOverlay.style.display = 'none'; | |
| }, | |
| /** | |
| * Handle file addition (drop or browse) | |
| * @param {Array<File>} newFiles - Array of new files | |
| */ | |
| async handleFileAddition(newFiles) { | |
| const isProcessingSuccessful = this.processFiles(newFiles); | |
| if (!isProcessingSuccessful) { | |
| return; | |
| } | |
| newFiles.forEach(file => StateManager.addFile(file)); | |
| // Upload files | |
| newFiles.forEach(async (file) => { | |
| file.state = 'uploading'; | |
| this.renderFiles(); | |
| const isUploadSuccessful = await ApiService.uploadFile(file); | |
| file.state = isUploadSuccessful ? 'uploaded' : 'ready'; | |
| this.renderFiles(); | |
| }); | |
| this.renderFiles(); | |
| }, | |
| /** | |
| * Validate files before adding | |
| * @param {Array<File>} newFiles - Array of files to validate | |
| * @returns {boolean} Whether files are valid | |
| */ | |
| processFiles(newFiles) { | |
| // Check file types | |
| const unallowedFiles = newFiles.filter((file) => | |
| !this.constants.ALLOWED_TYPES.some(ext => file.name.endsWith(ext)) | |
| ); | |
| if (unallowedFiles.length > 0) { | |
| newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); | |
| showSnackbar(translations[StateManager.currentLang]["error_file_format"], "error"); | |
| return false; | |
| } | |
| // Check individual file size | |
| const largeFiles = newFiles.filter((file) => file.size > this.constants.FILE_SIZE_LIMIT); | |
| if (largeFiles.length > 0) { | |
| newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); | |
| showSnackbar(translations[StateManager.currentLang]["error_file_size"], "error"); | |
| return false; | |
| } | |
| // Check total file size | |
| const totalFileSize = [...newFiles, ...StateManager.getFiles()].reduce((sum, file) => sum + file.size, 0); | |
| if (totalFileSize > this.constants.TOTAL_FILE_SIZE_LIMIT) { | |
| newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); | |
| showSnackbar(translations[StateManager.currentLang]["error_total_file_size"], "error"); | |
| return false; | |
| } | |
| // Check file name length | |
| const filesWithLongName = newFiles.filter((file) => file.name.length > this.constants.MAX_FILE_NAME_LENGTH); | |
| if (filesWithLongName.length > 0) { | |
| newFiles.forEach((file) => Utils.removeFileFromInput(this.elements.fileInput, file)); | |
| showSnackbar(translations[StateManager.currentLang]["error_file_name_length"], "error"); | |
| return false; | |
| } | |
| return true; | |
| }, | |
| /** | |
| * Render the file list | |
| */ | |
| renderFiles() { | |
| this.elements.fileListHtml.innerHTML = ''; | |
| const sessionFiles = StateManager.getFiles(); | |
| if (sessionFiles.length === 0) { | |
| const noFileMessage = document.createElement('div'); | |
| noFileMessage.classList.add('no-file'); | |
| noFileMessage.dataset.i18n = "no_files"; | |
| this.elements.fileListHtml.appendChild(noFileMessage); | |
| TranslationService.applyTranslation(); | |
| return; | |
| } | |
| sessionFiles.forEach((f) => { | |
| const fileItem = document.createElement('div'); | |
| fileItem.classList.add('file-item'); | |
| fileItem.textContent = f.name; | |
| const fileActions = document.createElement('div'); | |
| fileActions.classList.add('file-actions'); | |
| const uploadButton = this.createUploadButton(f); | |
| const deleteButton = this.createDeleteButton(f); | |
| fileActions.appendChild(uploadButton); | |
| fileActions.appendChild(deleteButton); | |
| fileItem.appendChild(fileActions); | |
| this.elements.fileListHtml.appendChild(fileItem); | |
| }); | |
| TranslationService.applyTranslation(); | |
| }, | |
| /** | |
| * Create upload button for a file | |
| * @param {File} file - File object | |
| * @returns {HTMLButtonElement} Upload button | |
| */ | |
| createUploadButton(file) { | |
| const uploadButton = document.createElement('button'); | |
| if (file.state === 'uploaded') { | |
| uploadButton.innerHTML = this.icons.check + `<span data-i18n="file_uploaded"></span>`; | |
| uploadButton.classList.add('disabled-button'); | |
| uploadButton.disabled = true; | |
| } else if (file.state === 'uploading') { | |
| uploadButton.innerHTML = this.icons.spinner + `<span data-i18n="file_uploading"></span>`; | |
| uploadButton.classList.add('disabled-button'); | |
| uploadButton.disabled = true; | |
| } else if (file.state === 'ready') { | |
| uploadButton.innerHTML = this.icons.upload + `<span data-i18n="file_upload"></span>`; | |
| uploadButton.classList.add('ok-button'); | |
| uploadButton.addEventListener('click', async () => { | |
| file.state = 'uploading'; | |
| this.renderFiles(); | |
| const isUploadSuccessful = await ApiService.uploadFile(file); | |
| file.state = isUploadSuccessful ? 'uploaded' : 'ready'; | |
| this.renderFiles(); | |
| }); | |
| } | |
| return uploadButton; | |
| }, | |
| /** | |
| * Create delete button for a file | |
| * @param {File} file - File object | |
| * @returns {HTMLButtonElement} Delete button | |
| */ | |
| createDeleteButton(file) { | |
| const deleteButton = document.createElement('button'); | |
| deleteButton.innerHTML = this.icons.trash + `<span data-i18n="file_delete"></span>`; | |
| deleteButton.classList.add('no-button'); | |
| deleteButton.addEventListener('click', async () => { | |
| // No need to send a request to the server if the file was not uploaded | |
| const isDeletionSuccessful = file.state === 'uploaded' | |
| ? await ApiService.deleteFile(file) | |
| : true; | |
| if (isDeletionSuccessful) { | |
| Utils.removeFileFromInput(this.elements.fileInput, file); | |
| StateManager.removeFile(file); | |
| this.renderFiles(); | |
| } | |
| }); | |
| return deleteButton; | |
| } | |
| }; |