Multitenant implementation with custom Domain, theme per vendor
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
// static/admin/js/vendor-theme.js
|
||||
/**
|
||||
* Vendor Theme Management Component
|
||||
* Follows the established Alpine.js pattern from FRONTEND_ALPINE_PAGE_TEMPLATE.md
|
||||
* Vendor Theme Editor - Alpine.js Component
|
||||
* Manages theme customization for vendor shops
|
||||
*/
|
||||
|
||||
const THEME_LOG_LEVEL = 3;
|
||||
// ============================================================================
|
||||
// LOGGING CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const THEME_LOG_LEVEL = 3; // 1=error, 2=warn, 3=info, 4=debug
|
||||
|
||||
const themeLog = {
|
||||
error: (...args) => THEME_LOG_LEVEL >= 1 && console.error('❌ [THEME ERROR]', ...args),
|
||||
@@ -13,71 +17,12 @@ const themeLog = {
|
||||
debug: (...args) => THEME_LOG_LEVEL >= 4 && console.log('🔍 [THEME DEBUG]', ...args)
|
||||
};
|
||||
|
||||
// Theme presets
|
||||
const THEME_PRESETS = {
|
||||
modern: {
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
secondary: "#8b5cf6",
|
||||
accent: "#ec4899"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "fixed"
|
||||
}
|
||||
},
|
||||
classic: {
|
||||
colors: {
|
||||
primary: "#1e40af",
|
||||
secondary: "#7c3aed",
|
||||
accent: "#dc2626"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Georgia, serif",
|
||||
body: "Arial, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "list",
|
||||
header: "static"
|
||||
}
|
||||
},
|
||||
minimal: {
|
||||
colors: {
|
||||
primary: "#000000",
|
||||
secondary: "#404040",
|
||||
accent: "#666666"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Helvetica, sans-serif",
|
||||
body: "Helvetica, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "transparent"
|
||||
}
|
||||
},
|
||||
vibrant: {
|
||||
colors: {
|
||||
primary: "#f59e0b",
|
||||
secondary: "#ef4444",
|
||||
accent: "#8b5cf6"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Poppins, sans-serif",
|
||||
body: "Open Sans, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "masonry",
|
||||
header: "fixed"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function vendorThemeData() {
|
||||
// ============================================================================
|
||||
// ALPINE.JS COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
function adminVendorTheme() {
|
||||
return {
|
||||
// ✅ CRITICAL: Inherit base layout functionality
|
||||
...data(),
|
||||
@@ -86,193 +31,279 @@ function vendorThemeData() {
|
||||
currentPage: 'vendor-theme',
|
||||
|
||||
// Page state
|
||||
loading: false,
|
||||
saving: false,
|
||||
vendorCode: null,
|
||||
vendor: null,
|
||||
vendorCode: window.location.pathname.split('/')[3], // Extract from /admin/vendors/{code}/theme
|
||||
loading: true,
|
||||
saving: false,
|
||||
error: null,
|
||||
|
||||
// Theme data
|
||||
// Theme data structure matching VendorTheme model
|
||||
themeData: {
|
||||
theme_name: 'default',
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
secondary: "#8b5cf6",
|
||||
accent: "#ec4899"
|
||||
primary: '#6366f1',
|
||||
secondary: '#8b5cf6',
|
||||
accent: '#ec4899',
|
||||
background: '#ffffff',
|
||||
text: '#1f2937',
|
||||
border: '#e5e7eb'
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif"
|
||||
heading: 'Inter, sans-serif',
|
||||
body: 'Inter, sans-serif'
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "fixed"
|
||||
style: 'grid',
|
||||
header: 'fixed',
|
||||
product_card: 'modern'
|
||||
},
|
||||
custom_css: ""
|
||||
branding: {
|
||||
logo: null,
|
||||
logo_dark: null,
|
||||
favicon: null,
|
||||
banner: null
|
||||
},
|
||||
custom_css: ''
|
||||
},
|
||||
|
||||
originalTheme: null, // For detecting changes
|
||||
// Available presets
|
||||
presets: [],
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
// ✅ CRITICAL: Proper initialization with guard
|
||||
async init() {
|
||||
themeLog.info('=== VENDOR THEME PAGE INITIALIZING ===');
|
||||
themeLog.info('=== VENDOR THEME EDITOR INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
// ✅ CRITICAL: Prevent multiple initializations
|
||||
if (window._vendorThemeInitialized) {
|
||||
themeLog.warn('Page already initialized, skipping...');
|
||||
themeLog.warn('Theme editor already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._vendorThemeInitialized = true;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get vendor code from URL
|
||||
this.vendorCode = this.getVendorCodeFromURL();
|
||||
themeLog.info('Vendor code:', this.vendorCode);
|
||||
|
||||
// Load data
|
||||
await this.loadVendor();
|
||||
await this.loadTheme();
|
||||
await Promise.all([
|
||||
this.loadVendorData(),
|
||||
this.loadTheme(),
|
||||
this.loadPresets()
|
||||
]);
|
||||
|
||||
themeLog.info('=== VENDOR THEME PAGE INITIALIZATION COMPLETE ===');
|
||||
const duration = Date.now() - startTime;
|
||||
themeLog.info(`=== THEME EDITOR INITIALIZATION COMPLETE (${duration}ms) ===`);
|
||||
},
|
||||
|
||||
// Load vendor info
|
||||
async loadVendor() {
|
||||
themeLog.info('Loading vendor:', this.vendorCode);
|
||||
// ============================================================================
|
||||
// URL HELPERS
|
||||
// ============================================================================
|
||||
|
||||
try {
|
||||
// ✅ CRITICAL: Use lowercase apiClient
|
||||
const response = await apiClient.get(`/api/v1/admin/vendors/${this.vendorCode}`);
|
||||
this.vendor = response;
|
||||
themeLog.info('Vendor loaded:', this.vendor.name);
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load vendor:', error);
|
||||
Utils.showToast('Failed to load vendor', 'error');
|
||||
}
|
||||
getVendorCodeFromURL() {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const vendorIndex = pathParts.indexOf('vendors');
|
||||
return pathParts[vendorIndex + 1];
|
||||
},
|
||||
|
||||
// Load theme configuration
|
||||
async loadTheme() {
|
||||
themeLog.info('Loading theme...');
|
||||
this.loading = true;
|
||||
// ============================================================================
|
||||
// DATA LOADING
|
||||
// ============================================================================
|
||||
|
||||
async loadVendorData() {
|
||||
themeLog.info('Loading vendor data...');
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Get vendor's theme config from vendor object
|
||||
if (this.vendor && this.vendor.theme_config) {
|
||||
this.themeData = {
|
||||
colors: this.vendor.theme_config.colors || this.themeData.colors,
|
||||
fonts: this.vendor.theme_config.fonts || this.themeData.fonts,
|
||||
layout: this.vendor.theme_config.layout || this.themeData.layout,
|
||||
custom_css: this.vendor.theme_config.custom_css || ""
|
||||
};
|
||||
} else {
|
||||
themeLog.info('No theme config found, using defaults');
|
||||
}
|
||||
|
||||
// Store original for change detection
|
||||
this.originalTheme = JSON.parse(JSON.stringify(this.themeData));
|
||||
|
||||
const response = await apiClient.get(`/api/v1/admin/vendors/${this.vendorCode}`);
|
||||
const duration = Date.now() - startTime;
|
||||
themeLog.info(`Theme loaded in ${duration}ms`, this.themeData);
|
||||
|
||||
this.vendor = response;
|
||||
themeLog.info(`Vendor loaded in ${duration}ms:`, this.vendor.name);
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load vendor:', error);
|
||||
this.error = 'Failed to load vendor data';
|
||||
Utils.showToast('Failed to load vendor data', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadTheme() {
|
||||
themeLog.info('Loading theme...');
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.get(`/api/v1/admin/vendor-themes/${this.vendorCode}`);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (response) {
|
||||
// Merge loaded theme with defaults
|
||||
this.themeData = {
|
||||
theme_name: response.theme_name || 'default',
|
||||
colors: {
|
||||
...this.themeData.colors,
|
||||
...(response.colors || {})
|
||||
},
|
||||
fonts: {
|
||||
heading: response.fonts?.heading || this.themeData.fonts.heading,
|
||||
body: response.fonts?.body || this.themeData.fonts.body
|
||||
},
|
||||
layout: {
|
||||
style: response.layout?.style || this.themeData.layout.style,
|
||||
header: response.layout?.header || this.themeData.layout.header,
|
||||
product_card: response.layout?.product_card || this.themeData.layout.product_card
|
||||
},
|
||||
branding: {
|
||||
...this.themeData.branding,
|
||||
...(response.branding || {})
|
||||
},
|
||||
custom_css: response.custom_css || ''
|
||||
};
|
||||
|
||||
themeLog.info(`Theme loaded in ${duration}ms:`, this.themeData.theme_name);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load theme:', error);
|
||||
this.error = 'Failed to load theme';
|
||||
Utils.showToast('Failed to load theme', 'error');
|
||||
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Save theme configuration
|
||||
async saveTheme() {
|
||||
themeLog.info('Saving theme...');
|
||||
async loadPresets() {
|
||||
themeLog.info('Loading presets...');
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.get('/api/v1/admin/vendor-themes/presets');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.presets = response.presets || [];
|
||||
themeLog.info(`${this.presets.length} presets loaded in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
themeLog.warn('Failed to load presets:', error);
|
||||
// Non-critical error, continue without presets
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// PRESET OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
async applyPreset(presetName) {
|
||||
themeLog.info(`Applying preset: ${presetName}`);
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Update vendor with new theme_config
|
||||
const updateData = {
|
||||
theme_config: this.themeData
|
||||
};
|
||||
|
||||
const response = await apiClient.put(
|
||||
`/api/v1/admin/vendors/${this.vendorCode}`,
|
||||
updateData
|
||||
const response = await apiClient.post(
|
||||
`/api/v1/admin/vendor-themes/${this.vendorCode}/preset/${presetName}`
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
themeLog.info(`Theme saved in ${duration}ms`);
|
||||
|
||||
// Update vendor data
|
||||
this.vendor = response;
|
||||
this.originalTheme = JSON.parse(JSON.stringify(this.themeData));
|
||||
if (response && response.theme) {
|
||||
// Update theme data with preset
|
||||
this.themeData = {
|
||||
theme_name: response.theme.theme_name,
|
||||
colors: response.theme.colors || this.themeData.colors,
|
||||
fonts: response.theme.fonts || this.themeData.fonts,
|
||||
layout: response.theme.layout || this.themeData.layout,
|
||||
branding: response.theme.branding || this.themeData.branding,
|
||||
custom_css: response.theme.custom_css || ''
|
||||
};
|
||||
|
||||
Utils.showToast('Theme saved successfully', 'success');
|
||||
Utils.showToast(`Applied ${presetName} preset successfully`, 'success');
|
||||
themeLog.info(`Preset applied in ${duration}ms`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to save theme:', error);
|
||||
Utils.showToast('Failed to save theme', 'error');
|
||||
themeLog.error('Failed to apply preset:', error);
|
||||
const message = error.response?.data?.detail || 'Failed to apply preset';
|
||||
Utils.showToast(message, 'error');
|
||||
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Apply preset theme
|
||||
applyPreset(presetName) {
|
||||
themeLog.info('Applying preset:', presetName);
|
||||
|
||||
if (!THEME_PRESETS[presetName]) {
|
||||
themeLog.error('Unknown preset:', presetName);
|
||||
async resetToDefault() {
|
||||
if (!confirm('Are you sure you want to reset to default theme? This will discard all customizations.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = THEME_PRESETS[presetName];
|
||||
|
||||
// Apply preset values
|
||||
this.themeData.colors = { ...preset.colors };
|
||||
this.themeData.fonts = { ...preset.fonts };
|
||||
this.themeData.layout = { ...preset.layout };
|
||||
|
||||
Utils.showToast(`Applied ${presetName} theme preset`, 'success');
|
||||
},
|
||||
|
||||
// Reset to default theme
|
||||
resetToDefault() {
|
||||
themeLog.info('Resetting to default theme');
|
||||
await this.applyPreset('default');
|
||||
},
|
||||
|
||||
// Confirm with user
|
||||
if (!confirm('Are you sure you want to reset to the default theme? This will discard all customizations.')) {
|
||||
return;
|
||||
// ============================================================================
|
||||
// SAVE OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
async saveTheme() {
|
||||
themeLog.info('Saving theme:', this.themeData);
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.put(
|
||||
`/api/v1/admin/vendor-themes/${this.vendorCode}`,
|
||||
this.themeData
|
||||
);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (response) {
|
||||
Utils.showToast('Theme saved successfully', 'success');
|
||||
themeLog.info(`Theme saved in ${duration}ms`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to save theme:', error);
|
||||
const message = error.response?.data?.detail || 'Failed to save theme';
|
||||
Utils.showToast(message, 'error');
|
||||
this.error = message;
|
||||
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
this.themeData = {
|
||||
colors: {
|
||||
primary: "#6366f1",
|
||||
secondary: "#8b5cf6",
|
||||
accent: "#ec4899"
|
||||
},
|
||||
fonts: {
|
||||
heading: "Inter, sans-serif",
|
||||
body: "Inter, sans-serif"
|
||||
},
|
||||
layout: {
|
||||
style: "grid",
|
||||
header: "fixed"
|
||||
},
|
||||
custom_css: ""
|
||||
};
|
||||
|
||||
Utils.showToast('Theme reset to default', 'info');
|
||||
},
|
||||
|
||||
// Check if theme has unsaved changes
|
||||
hasChanges() {
|
||||
if (!this.originalTheme) return false;
|
||||
return JSON.stringify(this.themeData) !== JSON.stringify(this.originalTheme);
|
||||
},
|
||||
// ============================================================================
|
||||
// HELPER METHODS
|
||||
// ============================================================================
|
||||
|
||||
// Format date helper
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
return Utils.formatDate(dateString);
|
||||
},
|
||||
|
||||
getPreviewStyle() {
|
||||
return {
|
||||
'--color-primary': this.themeData.colors.primary,
|
||||
'--color-secondary': this.themeData.colors.secondary,
|
||||
'--color-accent': this.themeData.colors.accent,
|
||||
'--color-background': this.themeData.colors.background,
|
||||
'--color-text': this.themeData.colors.text,
|
||||
'--color-border': this.themeData.colors.border,
|
||||
'--font-heading': this.themeData.fonts.heading,
|
||||
'--font-body': this.themeData.fonts.body,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
themeLog.info('Vendor theme module loaded');
|
||||
// ============================================================================
|
||||
// MODULE LOADED
|
||||
// ============================================================================
|
||||
|
||||
themeLog.info('Vendor theme editor module loaded');
|
||||
Reference in New Issue
Block a user