import { Logger } from '../../core/logger.js'; export const definition = { name: 'image-manager', version: '1.0.0', dependencies: [], provides: ['image-upload', 'image-gallery', 'image-modal'], priority: 5 }; const ImageManagerModule = { name: 'image-manager', activeGalleries: new Map(), activeUploaders: new Map(), async init(config = {}, state) { Logger.info('[ImageManager] Module initialized'); // Initialize all image gallery elements this.initializeGalleries(); // Initialize all image uploader elements this.initializeUploaders(); // Set up modal functionality this.initializeModal(); return this; }, initializeGalleries() { const galleryElements = document.querySelectorAll('[data-image-gallery]'); Logger.info(`[ImageManager] Found ${galleryElements.length} gallery elements`); galleryElements.forEach((element, index) => { const galleryId = `gallery-${index}`; const gallery = new ImageGallery(element, { listEndpoint: element.dataset.listEndpoint || '/api/images', pageSize: parseInt(element.dataset.pageSize) || 20, columns: parseInt(element.dataset.columns) || 4 }); this.activeGalleries.set(galleryId, gallery); Logger.info(`[ImageManager] Initialized gallery: ${galleryId}`); }); }, initializeUploaders() { const uploaderElements = document.querySelectorAll('[data-image-uploader]'); Logger.info(`[ImageManager] Found ${uploaderElements.length} uploader elements`); uploaderElements.forEach((element, index) => { const uploaderId = `uploader-${index}`; const uploader = new ImageUploader(element, { uploadUrl: element.dataset.uploadUrl || '/api/images', maxFileSize: parseInt(element.dataset.maxFileSize) || 10485760, allowedTypes: element.dataset.allowedTypes?.split(',') || ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], maxFiles: parseInt(element.dataset.maxFiles) || 10 }); this.activeUploaders.set(uploaderId, uploader); Logger.info(`[ImageManager] Initialized uploader: ${uploaderId}`); }); }, initializeModal() { const modal = new ImageModal(); window.ImageModal = modal; Logger.info('[ImageManager] Modal initialized'); }, destroy() { // Destroy all galleries this.activeGalleries.forEach((gallery, id) => { gallery.destroy(); Logger.info(`[ImageManager] Destroyed gallery: ${id}`); }); this.activeGalleries.clear(); // Destroy all uploaders this.activeUploaders.forEach((uploader, id) => { uploader.destroy(); Logger.info(`[ImageManager] Destroyed uploader: ${id}`); }); this.activeUploaders.clear(); // Clean up modal if (window.ImageModal) { window.ImageModal.destroy(); delete window.ImageModal; } Logger.info('[ImageManager] Module destroyed'); } }; // Image Gallery Implementation class ImageGallery { constructor(element, config) { this.element = element; this.config = config; this.currentPage = 1; this.images = []; this.loading = false; this.init(); } async init() { Logger.info('[ImageGallery] Initializing gallery'); // Clear loading state and build UI this.element.innerHTML = ''; this.buildGalleryUI(); // Load images await this.loadImages(); } buildGalleryUI() { this.element.innerHTML = ` `; // Add event listeners this.setupEventListeners(); } setupEventListeners() { const searchInput = this.element.querySelector('.gallery__search'); const searchClear = this.element.querySelector('.gallery__search-clear'); const sortSelect = this.element.querySelector('.gallery__sort'); const loadMoreBtn = this.element.querySelector('.gallery__load-more'); // Search functionality let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.searchImages(e.target.value); }, 300); }); searchClear.addEventListener('click', () => { searchInput.value = ''; this.searchImages(''); }); // Sort functionality sortSelect.addEventListener('change', (e) => { this.sortImages(e.target.value); }); // Load more functionality loadMoreBtn.addEventListener('click', () => { this.loadMoreImages(); }); } async loadImages() { if (this.loading) return; this.loading = true; this.showLoading(); try { Logger.info('[ImageGallery] Loading images from:', this.config.listEndpoint); const response = await fetch(`${this.config.listEndpoint}?page=${this.currentPage}&limit=${this.config.pageSize}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const response_data = await response.json(); Logger.info('[ImageGallery] API Response:', response_data); // Handle different API response formats let images = []; let metaInfo = {}; if (response_data.data && Array.isArray(response_data.data)) { // Format: {data: [...], meta: {...}} images = response_data.data; metaInfo = response_data.meta || {}; } else if (response_data.images && Array.isArray(response_data.images)) { // Format: {images: [...]} images = response_data.images; metaInfo = response_data; } else if (Array.isArray(response_data)) { // Format: [...] images = response_data; metaInfo = {}; } else { Logger.warn('[ImageGallery] Unexpected API response format:', response_data); images = []; metaInfo = {}; } Logger.info('[ImageGallery] Processed images:', images.length); if (this.currentPage === 1) { this.images = images; } else { this.images = [...this.images, ...images]; } this.renderImages(); this.updateLoadMoreButton(metaInfo); } catch (error) { Logger.error('[ImageGallery] Failed to load images:', error); this.showError(`Failed to load images: ${error.message}`); } finally { this.loading = false; this.hideLoading(); } } renderImages() { const grid = this.element.querySelector('.gallery__grid'); if (this.images.length === 0) { grid.innerHTML = ` `; return; } grid.innerHTML = this.images.map(image => this.renderImageItem(image)).join(''); // Add click handlers to gallery items grid.querySelectorAll('.gallery__item').forEach((item, index) => { item.addEventListener('click', () => { this.showImageModal(this.images[index]); }); }); } renderImageItem(image) { // Handle different API response formats for file size and dimensions let fileSize = '0 B'; let dimensions = '0 × 0'; if (image.file_size) { if (typeof image.file_size === 'object' && image.file_size.bytes) { fileSize = this.formatFileSize(image.file_size.bytes); } else if (typeof image.file_size === 'number') { fileSize = this.formatFileSize(image.file_size); } } if (image.dimensions && image.dimensions.width && image.dimensions.height) { dimensions = `${image.dimensions.width} × ${image.dimensions.height}`; } else if (image.width && image.height) { dimensions = `${image.width} × ${image.height}`; } return ` `; } showImageModal(image) { if (window.ImageModal) { window.ImageModal.show(image); } } formatFileSize(bytes) { const sizes = ['B', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 B'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${Math.round(bytes / Math.pow(1024, i) * 100) / 100} ${sizes[i]}`; } showLoading() { const loading = this.element.querySelector('.gallery__loading'); if (loading) loading.style.display = 'flex'; } hideLoading() { const loading = this.element.querySelector('.gallery__loading'); if (loading) loading.style.display = 'none'; } showError(message) { const grid = this.element.querySelector('.gallery__grid'); grid.innerHTML = ` `; } updateLoadMoreButton(metaInfo) { const loadMoreBtn = this.element.querySelector('.gallery__load-more'); const hasMore = metaInfo.has_more || metaInfo.hasMore || metaInfo.pagination?.hasMore || false; if (hasMore) { loadMoreBtn.style.display = 'block'; loadMoreBtn.disabled = false; } else { loadMoreBtn.style.display = 'none'; } } async loadMoreImages() { this.currentPage++; await this.loadImages(); } searchImages(query) { // Simple client-side search for now // TODO: Implement server-side search const filteredImages = this.images.filter(image => (image.filename || '').toLowerCase().includes(query.toLowerCase()) || (image.original_filename || '').toLowerCase().includes(query.toLowerCase()) || (image.alt_text || '').toLowerCase().includes(query.toLowerCase()) ); const grid = this.element.querySelector('.gallery__grid'); grid.innerHTML = filteredImages.map(image => this.renderImageItem(image)).join(''); } sortImages(sortType) { const sortedImages = [...this.images]; switch (sortType) { case 'created_desc': sortedImages.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); break; case 'created_asc': sortedImages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); break; case 'name_asc': sortedImages.sort((a, b) => (a.filename || '').localeCompare(b.filename || '')); break; case 'name_desc': sortedImages.sort((a, b) => (b.filename || '').localeCompare(a.filename || '')); break; } this.images = sortedImages; this.renderImages(); } destroy() { // Clean up event listeners and DOM this.element.innerHTML = ''; Logger.info('[ImageGallery] Gallery destroyed'); } } // Placeholder classes for uploader and modal class ImageUploader { constructor(element, config) { this.element = element; this.config = config; Logger.info('[ImageUploader] Uploader placeholder initialized'); } destroy() { Logger.info('[ImageUploader] Uploader destroyed'); } } class ImageModal { constructor() { Logger.info('[ImageModal] Modal placeholder initialized'); } show(image) { Logger.info('[ImageModal] Would show modal for image:', image); } destroy() { Logger.info('[ImageModal] Modal destroyed'); } } // Export for module system - following exact pattern from working modules export const init = ImageManagerModule.init.bind(ImageManagerModule); export const destroy = ImageManagerModule.destroy.bind(ImageManagerModule); export default ImageManagerModule;