mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #67
This commit is contained in:
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user