refactor: Extract UI into dedicated control panel module

- Create InsertrControlPanel class for unified UI management
- Separate business logic from presentation layer
- Remove DOM manipulation from auth.js and editor.js
- Add comprehensive CSS for status indicators and editing effects
- Implement consistent kebab-case file naming
- Add event-driven communication between core and UI layers

UI Architecture:
- Unified control panel with status indicator and action buttons
- Color-coded status dots (gray/blue/green for visitor/auth/editing)
- Professional editing hover effects with tooltips
- Responsive design for mobile devices
- Proper z-index and accessibility management

Business Logic:
- Pure auth.js focused on authentication state and OAuth flows
- Pure editor.js focused on content editing workflow
- Event emitters for state changes between modules
- Clean separation of concerns and testable architecture
This commit is contained in:
2025-09-17 16:52:49 +02:00
parent 10c755efca
commit 39b9c533fd
11 changed files with 886 additions and 290 deletions

371
lib/src/ui/control-panel.js Normal file
View File

@@ -0,0 +1,371 @@
/**
* InsertrControlPanel - Unified UI Controller
* Handles all presentation layer concerns for the insertr system
*
* Architecture:
* - Pure presentation layer - no business logic
* - Event-driven communication with core modules
* - Unified control panel design
* - Responsive and accessible
*/
export class InsertrControlPanel {
constructor(auth, editor, apiClient, options = {}) {
this.auth = auth;
this.editor = editor;
this.apiClient = apiClient;
this.options = options;
// UI state
this.elements = {};
this.isInitialized = false;
// Bind methods for event listeners
this.handleAuthToggle = this.handleAuthToggle.bind(this);
this.handleEditToggle = this.handleEditToggle.bind(this);
this.handleEnhanceClick = this.handleEnhanceClick.bind(this);
}
/**
* Initialize the control panel (called after DOM is ready)
*/
init() {
if (this.isInitialized) return;
console.log('🎨 Initializing Insertr Control Panel');
this.createControlPanel();
this.setupEventListeners();
this.updateVisualState();
this.isInitialized = true;
}
/**
* Create the unified control panel structure
*/
createControlPanel() {
// Check if control panel already exists
if (document.getElementById('insertr-control-panel')) {
this.cacheElements();
return;
}
const controlPanelHtml = `
<div id="insertr-control-panel" class="insertr-control-panel">
<!-- Status Indicator Section -->
<div id="insertr-status-section" class="insertr-status-section">
<div id="insertr-status-indicator" class="insertr-status-indicator">
<span class="insertr-status-text">Visitor Mode</span>
<span class="insertr-status-dot insertr-status-visitor"></span>
</div>
</div>
<!-- Action Buttons Section -->
<div id="insertr-action-section" class="insertr-action-section">
<button id="insertr-enhance-btn" class="insertr-action-btn insertr-enhance-btn"
style="display: none;"
title="Enhance files with latest content">
🔄 Enhance
</button>
<button id="insertr-edit-toggle" class="insertr-action-btn insertr-edit-btn"
style="display: none;">
Edit Mode: Off
</button>
<button id="insertr-auth-toggle" class="insertr-action-btn insertr-auth-btn">
Login as Client
</button>
</div>
</div>
`;
// Add control panel to page
document.body.insertAdjacentHTML('beforeend', controlPanelHtml);
this.cacheElements();
console.log('📱 Control panel created');
}
/**
* Cache DOM elements for performance
*/
cacheElements() {
this.elements = {
controlPanel: document.getElementById('insertr-control-panel'),
statusSection: document.getElementById('insertr-status-section'),
statusIndicator: document.getElementById('insertr-status-indicator'),
statusText: document.querySelector('.insertr-status-text'),
statusDot: document.querySelector('.insertr-status-dot'),
actionSection: document.getElementById('insertr-action-section'),
authToggle: document.getElementById('insertr-auth-toggle'),
editToggle: document.getElementById('insertr-edit-toggle'),
enhanceBtn: document.getElementById('insertr-enhance-btn')
};
}
/**
* Setup event listeners for all UI interactions
*/
setupEventListeners() {
if (this.elements.authToggle) {
this.elements.authToggle.addEventListener('click', this.handleAuthToggle);
}
if (this.elements.editToggle) {
this.elements.editToggle.addEventListener('click', this.handleEditToggle);
}
if (this.elements.enhanceBtn) {
this.elements.enhanceBtn.addEventListener('click', this.handleEnhanceClick);
}
// Listen for auth state changes
if (this.auth && typeof this.auth.on === 'function') {
this.auth.on('stateChange', () => this.updateVisualState());
}
// Listen for editor state changes
if (this.editor && typeof this.editor.on === 'function') {
this.editor.on('modeChange', () => this.updateVisualState());
}
}
/**
* Handle authentication toggle click
*/
handleAuthToggle() {
if (this.auth && typeof this.auth.toggleAuthentication === 'function') {
this.auth.toggleAuthentication();
this.updateVisualState();
}
}
/**
* Handle edit mode toggle click
*/
handleEditToggle() {
if (this.auth && typeof this.auth.toggleEditMode === 'function') {
this.auth.toggleEditMode();
this.updateVisualState();
}
}
/**
* Handle enhance button click
*/
async handleEnhanceClick() {
if (!this.elements.enhanceBtn) return;
const enhanceBtn = this.elements.enhanceBtn;
const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo';
try {
// Show loading state
enhanceBtn.textContent = '⏳ Enhancing...';
enhanceBtn.disabled = true;
// Smart server detection for enhance API
const isDevelopment = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
const enhanceUrl = isDevelopment
? `http://localhost:8080/api/enhance?site_id=${siteId}`
: `/api/enhance?site_id=${siteId}`;
// Call enhance API
const response = await fetch(enhanceUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.auth?.getCurrentUser()?.token || 'mock-token'}`
}
});
if (!response.ok) {
throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`);
}
const result = await response.json();
console.log('✅ Files enhanced successfully:', result);
// Show success state briefly
enhanceBtn.textContent = '✅ Enhanced!';
// Reset button after success
setTimeout(() => {
enhanceBtn.textContent = '🔄 Enhance';
enhanceBtn.disabled = false;
}, 2000);
} catch (error) {
console.error('❌ Enhancement failed:', error);
enhanceBtn.textContent = '❌ Failed';
// Reset button after error
setTimeout(() => {
enhanceBtn.textContent = '🔄 Enhance';
enhanceBtn.disabled = false;
}, 2000);
}
}
/**
* Update all visual elements based on current state
*/
updateVisualState() {
if (!this.isInitialized || !this.elements.statusText) return;
const isAuthenticated = this.auth ? this.auth.isAuthenticated() : false;
const isEditMode = this.auth ? this.auth.isEditMode() : false;
// Update status indicator
this.updateStatusIndicator(isAuthenticated, isEditMode);
// Update button states
this.updateButtonStates(isAuthenticated, isEditMode);
// Update body classes for global styling
this.updateBodyClasses(isAuthenticated, isEditMode);
}
/**
* Update status indicator text and visual state
*/
updateStatusIndicator(isAuthenticated, isEditMode) {
const { statusText, statusDot } = this.elements;
if (!statusText || !statusDot) return;
if (!isAuthenticated) {
statusText.textContent = 'Visitor Mode';
statusDot.className = 'insertr-status-dot insertr-status-visitor';
} else if (isEditMode) {
statusText.textContent = 'Editing';
statusDot.className = 'insertr-status-dot insertr-status-editing';
} else {
statusText.textContent = 'Authenticated';
statusDot.className = 'insertr-status-dot insertr-status-authenticated';
}
}
/**
* Update button text, visibility, and states
*/
updateButtonStates(isAuthenticated, isEditMode) {
const { authToggle, editToggle, enhanceBtn } = this.elements;
// Update auth toggle button
if (authToggle) {
authToggle.textContent = isAuthenticated ? 'Logout' : 'Login as Client';
authToggle.className = `insertr-action-btn insertr-auth-btn ${
isAuthenticated ? 'insertr-authenticated' : ''
}`;
}
// Update edit mode toggle button
if (editToggle) {
editToggle.style.display = isAuthenticated ? 'inline-block' : 'none';
editToggle.textContent = `Edit Mode: ${isEditMode ? 'On' : 'Off'}`;
editToggle.className = `insertr-action-btn insertr-edit-btn ${
isEditMode ? 'insertr-edit-active' : ''
}`;
}
// Update enhance button (dev-mode only)
if (enhanceBtn) {
enhanceBtn.style.display = isAuthenticated ? 'inline-block' : 'none';
}
}
/**
* Update body CSS classes for global styling hooks
*/
updateBodyClasses(isAuthenticated, isEditMode) {
document.body.classList.toggle('insertr-authenticated', isAuthenticated);
document.body.classList.toggle('insertr-edit-mode', isEditMode);
}
/**
* Add editing indicators to content elements
*/
addEditingIndicators() {
if (!this.editor || !this.editor.core) return;
const elements = this.editor.core.getAllElements();
console.log(`🎨 Adding editing indicators to ${elements.length} elements`);
elements.forEach(meta => {
this.initializeElementVisuals(meta.element);
});
}
/**
* Initialize visual editing indicators for a single element
*/
initializeElementVisuals(element) {
// Add visual indicators
element.style.cursor = 'pointer';
element.style.position = 'relative';
// Add hover effects
element.addEventListener('mouseenter', () => {
if (this.auth && this.auth.isAuthenticated() && this.auth.isEditMode()) {
element.classList.add('insertr-editing-hover');
}
});
element.addEventListener('mouseleave', () => {
element.classList.remove('insertr-editing-hover');
});
}
/**
* Remove editing indicators (when edit mode is disabled)
*/
removeEditingIndicators() {
const elements = document.querySelectorAll('.insertr-editing-hover');
elements.forEach(element => {
element.classList.remove('insertr-editing-hover');
});
}
/**
* Show status message to user
*/
showStatusMessage(message, type = 'info', duration = 3000) {
// Remove existing status message
const existing = document.getElementById('insertr-status-message');
if (existing) {
existing.remove();
}
const messageHtml = `
<div id="insertr-status-message" class="insertr-status-message ${type}">
<p class="insertr-status-text">${message}</p>
</div>
`;
document.body.insertAdjacentHTML('beforeend', messageHtml);
// Auto-remove after duration
setTimeout(() => {
const messageEl = document.getElementById('insertr-status-message');
if (messageEl) {
messageEl.remove();
}
}, duration);
}
/**
* Destroy the control panel (cleanup)
*/
destroy() {
if (this.elements.controlPanel) {
this.elements.controlPanel.remove();
}
this.elements = {};
this.isInitialized = false;
// Remove body classes
document.body.classList.remove('insertr-authenticated', 'insertr-edit-mode');
console.log('🗑️ Control panel destroyed');
}
}