This commit is contained in:
Tomas Dvorak
2025-10-21 15:02:05 +02:00
parent 68e69e00cc
commit 63700eedb2
103 changed files with 12442 additions and 446 deletions
+325
View File
@@ -0,0 +1,325 @@
/**
* MyUIbrix Editor Controller
* Manages editor state, variant changes, and real-time preview
*/
export interface ElementConfig {
element_name: string;
variant: string;
visible: boolean;
display_order: number;
custom_styles?: Record<string, any>;
}
export interface EditorState {
isEditing: boolean;
selectedElement: string | null;
configs: Record<string, ElementConfig>;
isDirty: boolean;
lastSaved: Date | null;
}
export class EditorController {
private state: EditorState;
private listeners: Set<(state: EditorState) => void>;
private saveTimeout: NodeJS.Timeout | null = null;
constructor() {
this.state = {
isEditing: false,
selectedElement: null,
configs: {},
isDirty: false,
lastSaved: null,
};
this.listeners = new Set();
}
/**
* Subscribe to state changes
*/
subscribe(listener: (state: EditorState) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
/**
* Get current state
*/
getState(): EditorState {
return { ...this.state };
}
/**
* Update variant for an element with immediate visual feedback
*/
updateVariant(elementName: string, variant: string): void {
console.log(`[EditorController] Updating variant: ${elementName} -> ${variant}`);
const config = this.state.configs[elementName] || {
element_name: elementName,
variant: 'default',
visible: true,
display_order: 0,
};
this.state = {
...this.state,
configs: {
...this.state.configs,
[elementName]: {
...config,
variant,
},
},
isDirty: true,
};
this.notifyListeners();
this.dispatchVariantChange(elementName, variant);
this.scheduleAutoSave();
}
/**
* Toggle element visibility
*/
toggleVisibility(elementName: string): void {
const config = this.state.configs[elementName];
if (!config) return;
const newVisible = !config.visible;
this.state = {
...this.state,
configs: {
...this.state.configs,
[elementName]: {
...config,
visible: newVisible,
},
},
isDirty: true,
};
this.notifyListeners();
this.dispatchVisibilityChange(elementName, newVisible);
this.scheduleAutoSave();
}
/**
* Reorder elements
*/
reorderElements(newOrder: string[]): void {
console.log('[EditorController] Reordering elements:', newOrder);
const updatedConfigs = { ...this.state.configs };
newOrder.forEach((elementName, index) => {
if (updatedConfigs[elementName]) {
updatedConfigs[elementName] = {
...updatedConfigs[elementName],
display_order: index,
};
}
});
this.state = {
...this.state,
configs: updatedConfigs,
isDirty: true,
};
this.notifyListeners();
this.dispatchReorderChange(newOrder);
this.scheduleAutoSave();
}
/**
* Update custom styles for an element
*/
updateStyles(elementName: string, styles: Record<string, any>): void {
const config = this.state.configs[elementName];
if (!config) return;
this.state = {
...this.state,
configs: {
...this.state.configs,
[elementName]: {
...config,
custom_styles: styles,
},
},
isDirty: true,
};
this.notifyListeners();
this.dispatchStyleChange(elementName, styles);
this.scheduleAutoSave();
}
/**
* Set selected element
*/
selectElement(elementName: string | null): void {
this.state = {
...this.state,
selectedElement: elementName,
};
this.notifyListeners();
}
/**
* Enter edit mode
*/
startEditing(initialConfigs: Record<string, ElementConfig>): void {
console.log('[EditorController] Starting edit mode');
this.state = {
...this.state,
isEditing: true,
configs: initialConfigs,
isDirty: false,
};
this.notifyListeners();
}
/**
* Exit edit mode
*/
stopEditing(): void {
console.log('[EditorController] Stopping edit mode');
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
this.saveTimeout = null;
}
this.state = {
...this.state,
isEditing: false,
selectedElement: null,
};
this.notifyListeners();
}
/**
* Save current state
*/
async save(): Promise<boolean> {
if (!this.state.isDirty) {
console.log('[EditorController] No changes to save');
return true;
}
try {
console.log('[EditorController] Saving changes...');
const configs = Object.values(this.state.configs);
// TODO: Replace with actual API call
const response = await fetch('/api/v1/admin/page-elements/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ configs }),
});
if (!response.ok) {
throw new Error('Save failed');
}
this.state = {
...this.state,
isDirty: false,
lastSaved: new Date(),
};
this.notifyListeners();
console.log('[EditorController] Changes saved successfully');
return true;
} catch (error) {
console.error('[EditorController] Save failed:', error);
return false;
}
}
/**
* Force refresh of all elements
*/
forceRefresh(): void {
console.log('[EditorController] Force refreshing all elements');
window.dispatchEvent(new CustomEvent('myuibrix-force-refresh', {
detail: { timestamp: Date.now() }
}));
}
// Private methods
private notifyListeners(): void {
this.listeners.forEach(listener => {
try {
listener(this.getState());
} catch (error) {
console.error('[EditorController] Listener error:', error);
}
});
}
private dispatchVariantChange(elementName: string, variant: string): void {
const config = this.state.configs[elementName];
window.dispatchEvent(new CustomEvent('myuibrix-change', {
detail: {
elementName,
variant,
visible: config?.visible ?? true,
previewMode: true,
timestamp: Date.now(),
}
}));
}
private dispatchVisibilityChange(elementName: string, visible: boolean): void {
const config = this.state.configs[elementName];
window.dispatchEvent(new CustomEvent('myuibrix-change', {
detail: {
elementName,
variant: config?.variant ?? 'default',
visible,
previewMode: true,
timestamp: Date.now(),
}
}));
}
private dispatchReorderChange(order: string[]): void {
window.dispatchEvent(new CustomEvent('myuibrix-reorder', {
detail: {
order,
previewMode: true,
timestamp: Date.now(),
}
}));
}
private dispatchStyleChange(elementName: string, styles: Record<string, any>): void {
window.dispatchEvent(new CustomEvent('myuibrix-style-change', {
detail: {
elementName,
styles,
previewMode: true,
timestamp: Date.now(),
}
}));
}
private scheduleAutoSave(): void {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
// Auto-save after 2 seconds of inactivity
this.saveTimeout = setTimeout(() => {
this.save();
}, 2000);
}
}
// Singleton instance
export const editorController = new EditorController();
+203
View File
@@ -0,0 +1,203 @@
/**
* MyUIbrix Backend API Service
* Provides validation and optimization for MyUIbrix editor
*/
import api from './api';
export interface ElementConfig {
page_type: string;
element_name: string;
variant: string;
visible: boolean;
display_order: number;
custom_styles?: Record<string, any>;
}
export interface ValidationResult {
valid: boolean;
optimized_styles?: Record<string, any>;
suggestions?: string[];
error?: string;
}
export interface OptimizationResult {
current_layout: any[];
suggestions: string[];
performance_score: number;
}
/**
* Validate a single element configuration
*/
export const validateElementConfig = async (config: ElementConfig): Promise<ValidationResult> => {
try {
const response = await api.post('/admin/myuibrix/validate', config);
return response.data;
} catch (error: any) {
console.error('Failed to validate element config:', error);
return {
valid: false,
error: error.response?.data?.error || 'Validation failed'
};
}
};
/**
* Validate multiple element configurations at once
*/
export const batchValidateConfigs = async (configs: ElementConfig[]): Promise<any> => {
try {
const response = await api.post('/admin/myuibrix/validate-batch', configs);
return response.data;
} catch (error: any) {
console.error('Failed to batch validate configs:', error);
throw error;
}
};
/**
* Get element preview metadata
*/
export const getElementPreview = async (
element: string,
variant: string,
viewport: 'desktop' | 'tablet' | 'mobile' = 'desktop'
): Promise<any> => {
try {
const response = await api.get('/admin/myuibrix/preview', {
params: { element, variant, viewport }
});
return response.data;
} catch (error: any) {
console.error('Failed to get element preview:', error);
throw error;
}
};
/**
* Get layout optimization suggestions
*/
export const optimizePageLayout = async (pageType: string = 'homepage'): Promise<OptimizationResult> => {
try {
const response = await api.get('/admin/myuibrix/optimize-layout', {
params: { page_type: pageType }
});
return response.data;
} catch (error: any) {
console.error('Failed to optimize layout:', error);
throw error;
}
};
/**
* Debounce helper for style changes
*/
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
/**
* Safe DOM manipulation wrapper
*/
export const safeDOM = {
/**
* Safely append child element
*/
appendChild: (parent: Element, child: Element): boolean => {
try {
if (!parent.contains(child)) {
parent.appendChild(child);
return true;
}
return false;
} catch (e) {
console.warn('Failed to append child:', e);
return false;
}
},
/**
* Safely remove child element
*/
removeChild: (parent: Element, child: Element): boolean => {
try {
if (parent.contains(child)) {
parent.removeChild(child);
return true;
}
return false;
} catch (e) {
console.warn('Failed to remove child:', e);
return false;
}
},
/**
* Safely replace child element
*/
replaceChild: (parent: Element, newChild: Element, oldChild: Element): boolean => {
try {
if (parent.contains(oldChild)) {
parent.replaceChild(newChild, oldChild);
return true;
}
return false;
} catch (e) {
console.warn('Failed to replace child:', e);
return false;
}
},
/**
* Safely query selector
*/
querySelector: (selector: string): Element | null => {
try {
return document.querySelector(selector);
} catch (e) {
console.warn('Invalid selector:', selector, e);
return null;
}
},
/**
* Safely query selector all
*/
querySelectorAll: (selector: string): Element[] => {
try {
return Array.from(document.querySelectorAll(selector));
} catch (e) {
console.warn('Invalid selector:', selector, e);
return [];
}
},
/**
* Safely insert before another element
*/
insertBefore: (parent: Element, newChild: Element, referenceChild: Node | null): boolean => {
try {
if (referenceChild && !parent.contains(referenceChild)) {
console.warn('Reference child is not a child of parent');
return false;
}
if (!parent.contains(newChild)) {
parent.insertBefore(newChild, referenceChild);
return true;
}
return false;
} catch (e) {
console.warn('Failed to insert before:', e);
return false;
}
}
};