// static/shared/js/media-picker.js /** * Media Picker Helper Functions * * Provides Alpine.js mixin for media library picker functionality. * Used in product create/edit forms to select images from store's media library. * * Usage: * In your Alpine component: * return { * ...mediaPickerMixin(storeIdGetter, multiSelect), * // your other data/methods * } */ // Use centralized logger const mediaPickerLog = window.LogConfig.loggers.mediaPicker || window.LogConfig.createLogger('mediaPicker', false); /** * Create media picker mixin for Alpine.js components * * @param {Function} storeIdGetter - Function that returns the current store ID * @param {boolean} multiSelect - Allow selecting multiple images * @returns {Object} Alpine.js mixin object */ function mediaPickerMixin(storeIdGetter, multiSelect = false) { return { // Modal visibility showMediaPicker: false, showMediaPickerAdditional: false, // Picker state mediaPickerState: { loading: false, uploading: false, media: [], selected: [], total: 0, skip: 0, limit: 24, search: '', }, // Which picker is active (main or additional) activePickerTarget: 'main', /** * Open media picker for main image */ openMediaPickerMain() { this.activePickerTarget = 'main'; this.mediaPickerState.selected = []; this.showMediaPicker = true; }, /** * Open media picker for additional images */ openMediaPickerAdditional() { this.activePickerTarget = 'additional'; this.mediaPickerState.selected = []; this.showMediaPickerAdditional = true; }, /** * Load media library from API */ async loadMediaLibrary() { const storeId = typeof storeIdGetter === 'function' ? storeIdGetter() : storeIdGetter; if (!storeId) { mediaPickerLog.warn('No store ID available'); return; } this.mediaPickerState.loading = true; this.mediaPickerState.skip = 0; try { const params = new URLSearchParams({ skip: '0', limit: this.mediaPickerState.limit.toString(), media_type: 'image', }); if (this.mediaPickerState.search) { params.append('search', this.mediaPickerState.search); } const response = await apiClient.get( `/admin/media/stores/${storeId}?${params.toString()}` ); this.mediaPickerState.media = response.media || []; this.mediaPickerState.total = response.total || 0; } catch (error) { mediaPickerLog.error('Failed to load media library:', error); window.dispatchEvent(new CustomEvent('toast', { detail: { message: 'Failed to load media library', type: 'error' } })); } finally { this.mediaPickerState.loading = false; } }, /** * Load more media (pagination) */ async loadMoreMedia() { const storeId = typeof storeIdGetter === 'function' ? storeIdGetter() : storeIdGetter; if (!storeId) return; this.mediaPickerState.loading = true; this.mediaPickerState.skip += this.mediaPickerState.limit; try { const params = new URLSearchParams({ skip: this.mediaPickerState.skip.toString(), limit: this.mediaPickerState.limit.toString(), media_type: 'image', }); if (this.mediaPickerState.search) { params.append('search', this.mediaPickerState.search); } const response = await apiClient.get( `/admin/media/stores/${storeId}?${params.toString()}` ); this.mediaPickerState.media = [ ...this.mediaPickerState.media, ...(response.media || []) ]; } catch (error) { mediaPickerLog.error('Failed to load more media:', error); } finally { this.mediaPickerState.loading = false; } }, /** * Upload a new media file */ async uploadMediaFile(event) { const file = event.target.files?.[0]; if (!file) return; const storeId = typeof storeIdGetter === 'function' ? storeIdGetter() : storeIdGetter; if (!storeId) { window.dispatchEvent(new CustomEvent('toast', { detail: { message: 'Please select a store first', type: 'error' } })); return; } // Validate file type if (!file.type.startsWith('image/')) { window.dispatchEvent(new CustomEvent('toast', { detail: { message: 'Please select an image file', type: 'error' } })); return; } // Validate file size (10MB max) if (file.size > 10 * 1024 * 1024) { window.dispatchEvent(new CustomEvent('toast', { detail: { message: 'Image must be less than 10MB', type: 'error' } })); return; } this.mediaPickerState.uploading = true; try { const formData = new FormData(); formData.append('file', file); const response = await apiClient.postFormData( `/admin/media/stores/${storeId}/upload?folder=products`, formData ); if (response.success && response.media) { // Add to beginning of media list this.mediaPickerState.media.unshift(response.media); this.mediaPickerState.total++; // Auto-select the uploaded image this.toggleMediaSelection(response.media); window.dispatchEvent(new CustomEvent('toast', { detail: { message: 'Image uploaded successfully', type: 'success' } })); } } catch (error) { mediaPickerLog.error('Failed to upload image:', error); window.dispatchEvent(new CustomEvent('toast', { detail: { message: error.message || 'Failed to upload image', type: 'error' } })); } finally { this.mediaPickerState.uploading = false; // Clear the file input event.target.value = ''; } }, /** * Toggle media selection */ toggleMediaSelection(media) { const index = this.mediaPickerState.selected.findIndex(m => m.id === media.id); if (index > -1) { // Deselect this.mediaPickerState.selected.splice(index, 1); } else { if (multiSelect) { // Multi-select: add to selection this.mediaPickerState.selected.push(media); } else { // Single-select: replace selection this.mediaPickerState.selected = [media]; } } }, /** * Check if media is selected */ isMediaSelected(mediaId) { return this.mediaPickerState.selected.some(m => m.id === mediaId); }, /** * Confirm selection and call the appropriate callback */ confirmMediaSelection() { const selected = this.mediaPickerState.selected; if (selected.length === 0) return; if (this.activePickerTarget === 'main') { // Main image: use first selected this.setMainImage(selected[0]); this.showMediaPicker = false; } else { // Additional images: add all selected this.addAdditionalImages(selected); this.showMediaPickerAdditional = false; } // Clear selection this.mediaPickerState.selected = []; }, /** * Set the main image (override in your component) */ setMainImage(media) { if (this.form) { this.form.primary_image_url = media.file_url; } mediaPickerLog.info('Main image set:', media.file_url); }, /** * Add additional images (override in your component) */ addAdditionalImages(mediaList) { if (this.form && Array.isArray(this.form.additional_images)) { const newUrls = mediaList.map(m => m.file_url); this.form.additional_images = [ ...this.form.additional_images, ...newUrls ]; } mediaPickerLog.info('Additional images added:', mediaList.map(m => m.file_url)); }, /** * Remove an additional image by index */ removeAdditionalImage(index) { if (this.form && Array.isArray(this.form.additional_images)) { this.form.additional_images.splice(index, 1); } }, /** * Clear the main image */ clearMainImage() { if (this.form) { this.form.primary_image_url = ''; } }, }; } // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = { mediaPickerMixin }; }