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 = `
No images found
Upload some images to get started
`;
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 `
${image.original_filename || image.filename}
${dimensions}
${fileSize}
${(image.mime_type || '').replace('image/', '')}
`;
}
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;