refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
307
app/modules/cms/static/shared/js/media-picker.js
Normal file
307
app/modules/cms/static/shared/js/media-picker.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// 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 vendor's media library.
|
||||
*
|
||||
* Usage:
|
||||
* In your Alpine component:
|
||||
* return {
|
||||
* ...mediaPickerMixin(vendorIdGetter, 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} vendorIdGetter - Function that returns the current vendor ID
|
||||
* @param {boolean} multiSelect - Allow selecting multiple images
|
||||
* @returns {Object} Alpine.js mixin object
|
||||
*/
|
||||
function mediaPickerMixin(vendorIdGetter, 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 vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
|
||||
|
||||
if (!vendorId) {
|
||||
mediaPickerLog.warn('No vendor 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/vendors/${vendorId}?${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 vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
|
||||
|
||||
if (!vendorId) 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/vendors/${vendorId}?${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 vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
|
||||
|
||||
if (!vendorId) {
|
||||
window.dispatchEvent(new CustomEvent('toast', {
|
||||
detail: { message: 'Please select a vendor 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/vendors/${vendorId}/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.url;
|
||||
}
|
||||
mediaPickerLog.info('Main image set:', media.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.url);
|
||||
this.form.additional_images = [
|
||||
...this.form.additional_images,
|
||||
...newUrls
|
||||
];
|
||||
}
|
||||
mediaPickerLog.info('Additional images added:', mediaList.map(m => m.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 };
|
||||
}
|
||||
Reference in New Issue
Block a user