/** * Modern Media Library - Reusable Component * Used across: Dashboard, Homepage, Products, Portfolio, Blog, Custom Pages, Settings */ class MediaLibrary { constructor(options = {}) { this.options = { multiple: options.multiple || false, onSelect: options.onSelect || null, selectMode: options.selectMode || false, containerId: options.containerId || "media-library-modal", ...options, }; this.currentFolder = null; this.folders = []; this.files = []; this.selectedItems = []; this.viewMode = localStorage.getItem("mediaLibraryView") || "grid"; this.sortBy = localStorage.getItem("mediaLibrarySort") || "date"; this.searchQuery = ""; this.draggedItem = null; this.modal = null; this.initialized = false; } // Initialize the media library modal init() { if (this.initialized) return; // Create modal HTML this.createModal(); this.bindEvents(); this.initialized = true; } createModal() { // Remove existing modal if any const existing = document.getElementById(this.options.containerId); if (existing) existing.remove(); const modalHtml = `
Preview
`; document.body.insertAdjacentHTML("beforeend", modalHtml); this.modal = new bootstrap.Modal( document.getElementById(this.options.containerId), ); } bindEvents() { const container = document.getElementById(this.options.containerId); // Back button document.getElementById("backBtn").addEventListener("click", () => { this.navigateBack(); }); // View toggle container.querySelectorAll(".view-toggle button").forEach((btn) => { btn.addEventListener("click", (e) => { this.setViewMode(e.currentTarget.dataset.view); }); }); // Sort change document.getElementById("sortSelect").addEventListener("change", (e) => { this.sortBy = e.target.value; localStorage.setItem("mediaLibrarySort", this.sortBy); this.renderContent(); }); // Search document .getElementById("mediaSearchInput") .addEventListener("input", (e) => { this.searchQuery = e.target.value.toLowerCase(); this.renderContent(); }); // Upload button document.getElementById("uploadBtn").addEventListener("click", () => { document.getElementById("fileInput").click(); }); // File input change document.getElementById("fileInput").addEventListener("change", (e) => { if (e.target.files.length > 0) { this.uploadFiles(e.target.files); } }); // Drop zone const dropZone = document.getElementById("uploadDropZone"); dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("dragover"); }); dropZone.addEventListener("dragleave", () => { dropZone.classList.remove("dragover"); }); dropZone.addEventListener("drop", (e) => { e.preventDefault(); dropZone.classList.remove("dragover"); if (e.dataTransfer.files.length > 0) { this.uploadFiles(e.dataTransfer.files); } }); dropZone.querySelector(".browse-link").addEventListener("click", () => { document.getElementById("fileInput").click(); }); // New folder button document.getElementById("newFolderBtn").addEventListener("click", () => { document.getElementById("newFolderInput").value = ""; new bootstrap.Modal(document.getElementById("createFolderModal")).show(); }); // Create folder confirm document .getElementById("confirmCreateFolderBtn") .addEventListener("click", () => { this.createFolder(); }); // Enter key on folder input document .getElementById("newFolderInput") .addEventListener("keypress", (e) => { if (e.key === "Enter") this.createFolder(); }); // Rename confirm document .getElementById("confirmRenameBtn") .addEventListener("click", () => { this.confirmRename(); }); // Enter key on rename input document.getElementById("renameInput").addEventListener("keypress", (e) => { if (e.key === "Enter") this.confirmRename(); }); // Delete selected document .getElementById("deleteSelectedBtn") .addEventListener("click", () => { this.deleteSelected(); }); // Select files button document.getElementById("selectFilesBtn").addEventListener("click", () => { this.confirmSelection(); }); // Image preview close document.getElementById("previewClose").addEventListener("click", () => { this.closePreview(); }); document .getElementById("imagePreviewOverlay") .addEventListener("click", (e) => { if (e.target.id === "imagePreviewOverlay") this.closePreview(); }); // Escape key to close preview document.addEventListener("keydown", (e) => { if (e.key === "Escape") this.closePreview(); }); } setViewMode(mode) { this.viewMode = mode; localStorage.setItem("mediaLibraryView", mode); const container = document.getElementById(this.options.containerId); container.querySelectorAll(".view-toggle button").forEach((btn) => { btn.classList.toggle("active", btn.dataset.view === mode); }); const content = document.getElementById("mediaContent"); content.classList.remove("grid-view", "list-view"); content.classList.add(mode === "list" ? "list-view" : "grid-view"); } async open() { this.init(); this.selectedItems = []; this.currentFolder = null; await this.loadContent(); this.modal.show(); this.updateSelectedCount(); } close() { if (this.modal) { this.modal.hide(); } } async loadContent() { try { const [foldersRes, filesRes] = await Promise.all([ fetch("/api/admin/folders", { credentials: "include" }), fetch( `/api/admin/uploads${ this.currentFolder ? `?folder_id=${this.currentFolder}` : "?folder_id=null" }`, { credentials: "include" }, ), ]); const foldersData = await foldersRes.json(); const filesData = await filesRes.json(); this.folders = foldersData.folders || []; this.files = filesData.files || []; this.renderContent(); this.updateBreadcrumb(); } catch (error) { console.error("Error loading media:", error); this.showNotification("Failed to load media library", "error"); } } renderContent() { const content = document.getElementById("mediaContent"); // Filter by search let filteredFolders = this.folders.filter((f) => this.currentFolder ? f.parentId === this.currentFolder : f.parentId === null, ); let filteredFiles = this.files; if (this.searchQuery) { filteredFolders = filteredFolders.filter((f) => f.name.toLowerCase().includes(this.searchQuery), ); filteredFiles = filteredFiles.filter( (f) => f.originalName.toLowerCase().includes(this.searchQuery) || f.filename.toLowerCase().includes(this.searchQuery), ); } // Sort files filteredFiles = this.sortFiles(filteredFiles); if (filteredFolders.length === 0 && filteredFiles.length === 0) { content.innerHTML = `
${ this.searchQuery ? "No results found" : "This folder is empty" }

${ this.searchQuery ? "Try a different search term" : "Upload files or create a folder to get started" }

`; return; } let html = ""; // Render folders first filteredFolders.forEach((folder) => { const isSelected = this.selectedItems.some( (s) => s.type === "folder" && s.id === folder.id, ); html += `
${this.escapeHtml(folder.name)} ${folder.fileCount || 0} files
`; }); // Render files filteredFiles.forEach((file) => { const isSelected = this.selectedItems.some( (s) => s.type === "file" && s.id === file.id, ); const fileSize = this.formatFileSize(file.size); const isVideo = this.isVideoFile(file.originalName || file.path); const previewHtml = isVideo ? `
Video
` : `${file.originalName}`; html += `
${previewHtml}
${this.escapeHtml(file.originalName)} ${fileSize}
`; }); content.innerHTML = html; this.bindItemEvents(); } bindItemEvents() { const content = document.getElementById("mediaContent"); content.querySelectorAll(".media-item").forEach((item) => { // Click to select item.addEventListener("click", (e) => { if ( e.target.closest(".item-actions") || e.target.closest(".item-checkbox") ) return; const type = item.dataset.type; const id = parseInt(item.dataset.id); // Double click to open folder or preview image if (type === "folder") { this.currentFolder = id; this.loadContent(); return; } // Single click to select/deselect this.toggleSelection(item); }); // Checkbox click item .querySelector(".item-checkbox input") .addEventListener("change", (e) => { e.stopPropagation(); this.toggleSelection(item); }); // Action buttons item.querySelectorAll(".btn-action").forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); const action = btn.dataset.action; const type = item.dataset.type; const id = parseInt(item.dataset.id); const name = item.dataset.name; switch (action) { case "preview": this.showPreview(item.dataset.path, name, item.dataset.size); break; case "rename": this.showRenameModal(id, type, name); break; case "delete": this.deleteItem(id, type, name); break; } }); }); // Drag and drop item.addEventListener("dragstart", (e) => { this.draggedItem = { type: item.dataset.type, id: parseInt(item.dataset.id), }; item.classList.add("dragging"); }); item.addEventListener("dragend", () => { item.classList.remove("dragging"); this.draggedItem = null; }); if (item.dataset.type === "folder") { item.addEventListener("dragover", (e) => { e.preventDefault(); if (this.draggedItem && this.draggedItem.type === "file") { item.classList.add("drag-over"); } }); item.addEventListener("dragleave", () => { item.classList.remove("drag-over"); }); item.addEventListener("drop", (e) => { e.preventDefault(); item.classList.remove("drag-over"); if (this.draggedItem && this.draggedItem.type === "file") { this.moveFileToFolder( this.draggedItem.id, parseInt(item.dataset.id), ); } }); } }); } toggleSelection(item) { const type = item.dataset.type; const id = parseInt(item.dataset.id); const path = item.dataset.path; const name = item.dataset.name; // Only allow selecting files in select mode if (this.options.selectMode && type === "folder") { // Navigate into folder instead this.currentFolder = id; this.loadContent(); return; } const existingIndex = this.selectedItems.findIndex( (s) => s.type === type && s.id === id, ); if (existingIndex >= 0) { this.selectedItems.splice(existingIndex, 1); item.classList.remove("selected"); item.querySelector(".item-checkbox input").checked = false; } else { if (!this.options.multiple && this.options.selectMode) { // Single select mode - clear previous selection this.selectedItems = []; document.querySelectorAll(".media-item.selected").forEach((el) => { el.classList.remove("selected"); el.querySelector(".item-checkbox input").checked = false; }); } this.selectedItems.push({ type, id, path, name }); item.classList.add("selected"); item.querySelector(".item-checkbox input").checked = true; } this.updateSelectedCount(); } updateSelectedCount() { const count = this.selectedItems.filter((s) => s.type === "file").length; const countEl = document.getElementById("selectedCount"); const deleteBtn = document.getElementById("deleteSelectedBtn"); const selectBtn = document.getElementById("selectFilesBtn"); if (count > 0) { countEl.style.display = "inline-flex"; countEl.querySelector(".count").textContent = count; deleteBtn.style.display = "inline-flex"; if (this.options.selectMode) { selectBtn.disabled = false; } } else { countEl.style.display = "none"; deleteBtn.style.display = "none"; if (this.options.selectMode) { selectBtn.disabled = true; } } } sortFiles(files) { const sorted = [...files]; switch (this.sortBy) { case "date": sorted.sort((a, b) => new Date(b.uploadDate) - new Date(a.uploadDate)); break; case "date-asc": sorted.sort((a, b) => new Date(a.uploadDate) - new Date(b.uploadDate)); break; case "name": sorted.sort((a, b) => a.originalName.localeCompare(b.originalName)); break; case "name-desc": sorted.sort((a, b) => b.originalName.localeCompare(a.originalName)); break; case "size": sorted.sort((a, b) => b.size - a.size); break; case "size-asc": sorted.sort((a, b) => a.size - b.size); break; } return sorted; } updateBreadcrumb() { const breadcrumb = document.getElementById("folderBreadcrumb"); const backBtn = document.getElementById("backBtn"); let html = ``; // Show/hide back button based on current folder if (this.currentFolder) { backBtn.style.display = "inline-flex"; } else { backBtn.style.display = "none"; } if (this.currentFolder) { const folder = this.folders.find((f) => f.id === this.currentFolder); if (folder) { const pathParts = folder.path.split("/").filter((p) => p); let currentPath = ""; pathParts.forEach((part, index) => { currentPath += "/" + part; const pathFolder = this.folders.find((f) => f.path === currentPath); const isLast = index === pathParts.length - 1; if (isLast) { html += ``; } else if (pathFolder) { html += ``; } }); } } breadcrumb.innerHTML = html; // Bind breadcrumb clicks breadcrumb.querySelectorAll("a").forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const folderId = link.dataset.folder; this.currentFolder = folderId === "null" ? null : parseInt(folderId); this.loadContent(); }); }); } navigateBack() { if (!this.currentFolder) return; // Find the parent folder const currentFolderObj = this.folders.find( (f) => f.id === this.currentFolder, ); if (currentFolderObj && currentFolderObj.parentId) { this.currentFolder = currentFolderObj.parentId; } else { this.currentFolder = null; } this.loadContent(); } async uploadFiles(fileList) { const files = Array.from(fileList); const progressEl = document.getElementById("uploadProgress"); const progressBar = progressEl.querySelector(".progress-bar"); const progressPercent = progressEl.querySelector(".progress-percent"); progressEl.style.display = "block"; progressBar.style.width = "0%"; const formData = new FormData(); files.forEach((file) => formData.append("files", file)); if (this.currentFolder) { formData.append("folder_id", this.currentFolder); } try { const response = await fetch("/api/admin/upload", { method: "POST", body: formData, credentials: "include", }); const data = await response.json(); if (data.success) { progressBar.style.width = "100%"; progressPercent.textContent = "100%"; this.showNotification( `${data.files.length} file(s) uploaded successfully`, "success", ); await this.loadContent(); } else { throw new Error(data.message || "Upload failed"); } } catch (error) { console.error("Upload error:", error); this.showNotification(error.message || "Upload failed", "error"); } finally { setTimeout(() => { progressEl.style.display = "none"; progressBar.style.width = "0%"; }, 1000); document.getElementById("fileInput").value = ""; } } async createFolder() { const input = document.getElementById("newFolderInput"); const name = input.value.trim(); if (!name) { this.showNotification("Please enter a folder name", "error"); return; } try { const response = await fetch("/api/admin/folders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name, parent_id: this.currentFolder, }), credentials: "include", }); const data = await response.json(); if (data.success) { bootstrap.Modal.getInstance( document.getElementById("createFolderModal"), ).hide(); this.showNotification("Folder created successfully", "success"); await this.loadContent(); } else { throw new Error(data.error || "Failed to create folder"); } } catch (error) { console.error("Create folder error:", error); this.showNotification( error.message || "Failed to create folder", "error", ); } } showRenameModal(id, type, currentName) { document.getElementById("renameItemId").value = id; document.getElementById("renameItemType").value = type; // Remove extension for files let displayName = currentName; if (type === "file") { const lastDot = currentName.lastIndexOf("."); if (lastDot > 0) { displayName = currentName.substring(0, lastDot); } } document.getElementById("renameInput").value = displayName; new bootstrap.Modal(document.getElementById("renameModal")).show(); setTimeout(() => { document.getElementById("renameInput").select(); }, 100); } async confirmRename() { const id = parseInt(document.getElementById("renameItemId").value); const type = document.getElementById("renameItemType").value; const newName = document.getElementById("renameInput").value.trim(); if (!newName) { this.showNotification("Please enter a name", "error"); return; } try { const endpoint = type === "folder" ? `/api/admin/folders/${id}/rename` : `/api/admin/uploads/${id}/rename`; const response = await fetch(endpoint, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ newName }), credentials: "include", }); const data = await response.json(); if (data.success) { bootstrap.Modal.getInstance( document.getElementById("renameModal"), ).hide(); this.showNotification("Renamed successfully", "success"); await this.loadContent(); } else { throw new Error(data.error || "Failed to rename"); } } catch (error) { console.error("Rename error:", error); this.showNotification(error.message || "Failed to rename", "error"); } } async deleteItem(id, type, name) { const self = this; showDeleteConfirm( `Are you sure you want to delete "${name}"?`, async () => { try { const endpoint = type === "folder" ? `/api/admin/folders/${id}?delete_contents=true` : `/api/admin/uploads/id/${id}`; const response = await fetch(endpoint, { method: "DELETE", credentials: "include", }); const data = await response.json(); if (data.success) { self.showNotification("Deleted successfully", "success"); // Remove from selected items self.selectedItems = self.selectedItems.filter( (s) => !(s.type === type && s.id === id), ); self.updateSelectedCount(); await self.loadContent(); } else { throw new Error(data.error || "Failed to delete"); } } catch (error) { console.error("Delete error:", error); self.showNotification(error.message || "Failed to delete", "error"); } }, { title: "Delete Item", confirmText: "Delete" }, ); } async deleteSelected() { const fileIds = this.selectedItems .filter((s) => s.type === "file") .map((s) => s.id); const folderIds = this.selectedItems .filter((s) => s.type === "folder") .map((s) => s.id); if (fileIds.length === 0 && folderIds.length === 0) return; const total = fileIds.length + folderIds.length; const self = this; showDeleteConfirm( `Are you sure you want to delete ${total} item(s)?`, async () => { try { // Delete folders first for (const folderId of folderIds) { await fetch(`/api/admin/folders/${folderId}?delete_contents=true`, { method: "DELETE", credentials: "include", }); } // Delete files if (fileIds.length > 0) { await fetch("/api/admin/uploads/bulk-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_ids: fileIds }), credentials: "include", }); } self.showNotification(`${total} item(s) deleted`, "success"); self.selectedItems = []; self.updateSelectedCount(); await self.loadContent(); } catch (error) { console.error("Bulk delete error:", error); self.showNotification("Failed to delete some items", "error"); } }, { title: "Delete Items", confirmText: `Delete ${total} Items` }, ); } async moveFileToFolder(fileId, folderId) { try { const response = await fetch("/api/admin/uploads/move", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_ids: [fileId], folder_id: folderId, }), credentials: "include", }); const data = await response.json(); if (data.success) { this.showNotification("File moved successfully", "success"); await this.loadContent(); } else { throw new Error(data.error || "Failed to move file"); } } catch (error) { console.error("Move error:", error); this.showNotification(error.message || "Failed to move file", "error"); } } confirmSelection() { const selectedFiles = this.selectedItems.filter((s) => s.type === "file"); if (selectedFiles.length === 0) { this.showNotification("Please select at least one file", "error"); return; } if (this.options.onSelect) { const result = this.options.multiple ? selectedFiles : selectedFiles[0]; this.options.onSelect(result); } this.close(); } showPreview(path, name, size) { const overlay = document.getElementById("imagePreviewOverlay"); document.getElementById("previewImage").src = path; document.getElementById("previewFilename").textContent = name; document.getElementById("previewSize").textContent = this.formatFileSize( parseInt(size), ); overlay.classList.add("active"); document.body.style.overflow = "hidden"; } closePreview() { const overlay = document.getElementById("imagePreviewOverlay"); overlay.classList.remove("active"); document.body.style.overflow = ""; } showNotification(message, type = "info") { // Create toast notification const toast = document.createElement("div"); toast.className = `media-toast ${type}`; toast.innerHTML = ` ${this.escapeHtml(message)} `; document.body.appendChild(toast); setTimeout(() => toast.classList.add("show"), 10); setTimeout(() => { toast.classList.remove("show"); setTimeout(() => toast.remove(), 300); }, 3000); } formatFileSize(bytes) { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } isVideoFile(filename) { if (!filename) return false; const ext = filename.toLowerCase().split(".").pop(); return ["mp4", "webm", "mov", "avi", "mkv"].includes(ext); } escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } } // Export for use in other files window.MediaLibrary = MediaLibrary;