// app/modules/cms/static/store/js/media.js /** * Store media library management page logic * Upload and manage images, videos, and documents */ const storeMediaLog = window.LogConfig.loggers.storeMedia || window.LogConfig.createLogger('storeMedia', false); storeMediaLog.info('Loading...'); function storeMedia() { storeMediaLog.info('storeMedia() called'); return { // Inherit base layout state ...data(), // Set page identifier currentPage: 'media', // Loading states loading: true, error: '', saving: false, // Media data media: [], stats: { total: 0, images: 0, videos: 0, documents: 0 }, // Filters filters: { search: '', type: '', folder: '' }, // Pagination pagination: { page: 1, per_page: 24, total: 0, pages: 0 }, // Computed pagination properties required by pagination macro get startIndex() { if (this.pagination.total === 0) return 0; return (this.pagination.page - 1) * this.pagination.per_page + 1; }, get endIndex() { return Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total); }, get totalPages() { return this.pagination.pages; }, get pageNumbers() { const pages = []; const total = this.pagination.pages; const current = this.pagination.page; if (total <= 7) { for (let i = 1; i <= total; i++) pages.push(i); } else { pages.push(1); if (current > 3) pages.push('...'); for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) { pages.push(i); } if (current < total - 2) pages.push('...'); pages.push(total); } return pages; }, previousPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadMedia(); } }, nextPage() { if (this.pagination.page < this.pagination.pages) { this.pagination.page++; this.loadMedia(); } }, goToPage(pageNum) { if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.pagination.pages) { this.pagination.page = pageNum; this.loadMedia(); } }, // Modal states showUploadModal: false, showDetailModal: false, showDeleteMediaConfirm: false, selectedMedia: null, editingMedia: { filename: '', alt_text: '', description: '', folder: '' }, // Upload states isDragging: false, uploadFolder: 'general', uploadingFiles: [], async init() { // Load i18n translations await I18n.loadModule('cms'); // Guard against duplicate initialization if (window._storeMediaInitialized) return; window._storeMediaInitialized = true; storeMediaLog.info('Initializing media library...'); try { // IMPORTANT: Call parent init first to set storeCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } // Initialize pagination per_page from PlatformSettings if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } await this.loadMedia(); } catch (err) { storeMediaLog.error('Failed to initialize media library:', err); this.error = err.message || 'Failed to initialize media library'; this.loading = false; } }, async loadMedia() { this.loading = true; this.error = ''; try { const params = new URLSearchParams({ skip: (this.pagination.page - 1) * this.pagination.per_page, limit: this.pagination.per_page }); if (this.filters.search) { params.append('search', this.filters.search); } if (this.filters.type) { params.append('media_type', this.filters.type); } if (this.filters.folder) { params.append('folder', this.filters.folder); } storeMediaLog.info(`Loading media: /api/v1/store/media?${params}`); const response = await apiClient.get(`/store/media?${params.toString()}`); if (response.ok) { const data = response.data; this.media = data.media || []; this.pagination.total = data.total || 0; this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page); // Update stats await this.loadStats(); storeMediaLog.info(`Loaded ${this.media.length} media files`); } else { throw new Error(response.message || 'Failed to load media'); } } catch (err) { storeMediaLog.error('Failed to load media:', err); this.error = err.message || 'Failed to load media library'; } finally { this.loading = false; } }, async loadStats() { // Calculate stats from loaded media (simplified) // In production, you might have a separate stats endpoint try { // Get all media without pagination for stats const allResponse = await apiClient.get('/store/media?limit=1000'); if (allResponse.ok) { const allMedia = allResponse.data.media || []; this.stats.total = allResponse.data.total || 0; this.stats.images = allMedia.filter(m => m.media_type === 'image').length; this.stats.videos = allMedia.filter(m => m.media_type === 'video').length; this.stats.documents = allMedia.filter(m => m.media_type === 'document').length; } } catch (err) { storeMediaLog.warn('Could not load stats:', err); } }, selectMedia(item) { this.selectedMedia = item; this.editingMedia = { filename: item.original_filename || item.filename, alt_text: item.alt_text || '', description: item.description || '', folder: item.folder || 'general' }; this.showDetailModal = true; }, async saveMediaDetails() { if (!this.selectedMedia) return; this.saving = true; try { const response = await apiClient.put(`/store/media/${this.selectedMedia.id}`, { filename: this.editingMedia.filename, alt_text: this.editingMedia.alt_text, description: this.editingMedia.description, folder: this.editingMedia.folder }); if (response.ok) { Utils.showToast(I18n.t('cms.messages.media_updated_successfully'), 'success'); this.showDetailModal = false; await this.loadMedia(); } else { throw new Error(response.message || 'Failed to update media'); } } catch (err) { storeMediaLog.error('Failed to save media:', err); this.showToast(err.message || 'Failed to save changes', 'error'); } finally { this.saving = false; } }, async deleteMedia() { if (!this.selectedMedia) return; this.saving = true; try { const response = await apiClient.delete(`/store/media/${this.selectedMedia.id}`); if (response.ok) { Utils.showToast(I18n.t('cms.messages.media_deleted_successfully'), 'success'); this.showDetailModal = false; this.selectedMedia = null; await this.loadMedia(); } else { throw new Error(response.message || 'Failed to delete media'); } } catch (err) { storeMediaLog.error('Failed to delete media:', err); this.showToast(err.message || 'Failed to delete media', 'error'); } finally { this.saving = false; } }, handleDrop(event) { this.isDragging = false; const files = event.dataTransfer.files; if (files.length) { this.uploadFiles(files); } }, handleFileSelect(event) { const files = event.target.files; if (files.length) { this.uploadFiles(files); } // Reset input event.target.value = ''; }, async uploadFiles(files) { storeMediaLog.info(`Uploading ${files.length} files...`); for (const file of files) { const uploadItem = { name: file.name, status: 'uploading', error: null }; this.uploadingFiles.push(uploadItem); try { const formData = new FormData(); formData.append('file', file); // Use apiClient.postFormData for automatic auth handling const response = await apiClient.postFormData( `/store/media/upload?folder=${this.uploadFolder}`, formData ); if (response.ok) { uploadItem.status = 'success'; storeMediaLog.info(`Uploaded: ${file.name}`); } else { uploadItem.status = 'error'; uploadItem.error = response.message || 'Upload failed'; storeMediaLog.error(`Upload failed for ${file.name}:`, response); } } catch (err) { uploadItem.status = 'error'; uploadItem.error = err.message || 'Upload failed'; storeMediaLog.error(`Upload error for ${file.name}:`, err); } } // Refresh media list after all uploads await this.loadMedia(); // Clear upload list after a delay setTimeout(() => { this.uploadingFiles = this.uploadingFiles.filter(f => f.status === 'uploading'); }, 3000); }, formatFileSize(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; }, copyToClipboard(text) { if (!text) return; navigator.clipboard.writeText(text).then(() => { Utils.showToast(I18n.t('cms.messages.url_copied_to_clipboard'), 'success'); }).catch(() => { Utils.showToast(I18n.t('cms.messages.failed_to_copy_url'), 'error'); }); } }; } storeMediaLog.info('Loaded successfully');