Files
michaelschiemer/resources/js/modules/image-manager/index.js
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

446 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `
<div class="image-gallery">
<div class="gallery__controls">
<div class="gallery__search-wrapper">
<input type="text" class="gallery__search" placeholder="Search images...">
<button type="button" class="gallery__search-clear">&times;</button>
</div>
<div class="gallery__sort-wrapper">
<label>Sort:</label>
<select class="gallery__sort">
<option value="created_desc">Newest First</option>
<option value="created_asc">Oldest First</option>
<option value="name_asc">Name A-Z</option>
<option value="name_desc">Name Z-A</option>
</select>
</div>
</div>
<div class="gallery__grid" style="--columns: ${this.config.columns}"></div>
<div class="gallery__pagination">
<button type="button" class="gallery__load-more" style="display: none;">Load More</button>
</div>
<div class="gallery__loading">
<div class="loading-spinner"></div>
<p>Loading images...</p>
</div>
</div>
`;
// 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 = `
<div class="gallery__empty">
<div class="empty-state__icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<circle cx="8.5" cy="8.5" r="1.5"></circle>
<polyline points="21,15 16,10 5,21"></polyline>
</svg>
</div>
<h3>No images found</h3>
<p>Upload some images to get started</p>
</div>
`;
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 `
<div class="gallery__item" data-image-id="${image.ulid}">
<div class="gallery__item-inner">
<div class="gallery__item-image">
<img src="${image.url}" alt="${image.alt_text || image.filename}" loading="lazy">
<div class="gallery__item-overlay">
<div class="gallery__item-actions">
<button type="button" class="action-btn" title="View Details">👁</button>
<button type="button" class="action-btn" title="Edit">✏️</button>
<button type="button" class="action-btn" title="Delete">🗑</button>
</div>
</div>
</div>
<div class="gallery__item-info">
<div class="gallery__item-name" title="${image.original_filename || image.filename}">
${image.original_filename || image.filename}
</div>
<div class="gallery__item-meta">
<span class="meta-item">${dimensions}</span>
<span class="meta-item">${fileSize}</span>
<span class="meta-item">${(image.mime_type || '').replace('image/', '')}</span>
</div>
</div>
</div>
</div>
`;
}
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 = `
<div class="gallery__error">
<div class="error-message">
<span>⚠️</span>
<span>${message}</span>
<button type="button" class="error-close" onclick="this.parentElement.parentElement.remove()">&times;</button>
</div>
</div>
`;
}
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;