// 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: ``,
spinner: ``,
check: ``,
trash: ``
},
/**
* 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} 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} 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 + ``;
uploadButton.classList.add('disabled-button');
uploadButton.disabled = true;
} else if (file.state === 'uploading') {
uploadButton.innerHTML = this.icons.spinner + ``;
uploadButton.classList.add('disabled-button');
uploadButton.disabled = true;
} else if (file.state === 'ready') {
uploadButton.innerHTML = this.icons.upload + ``;
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 + ``;
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;
}
};