- renombrar openspec/ a _openspec/ (carpeta auxiliar) - mover specs de features a changes/ - crear specs base: arquitectura-limpia, estandares-codigo, nomenclatura - migrar _planificacion/ con design-system y roi-theme-template - agregar especificacion recaptcha anti-spam (proposal, tasks, spec) - corregir rutas y referencias en todas las specs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
15 KiB
💻 JAVASCRIPT PATTERNS
1. Inicialización del Componente
document.addEventListener('DOMContentLoaded', function() {
console.log('✅ [ComponentName] Admin Panel cargado');
// 1. Cargar configuración guardada
loadConfig();
// 2. Inicializar vista previa
updatePreview();
// 3. Conectar event listeners
initializeEventListeners();
});
Orden de ejecución:
loadConfig(): Carga valores guardados desde localStorage/JSONupdatePreview(): Renderiza el preview inicialinitializeEventListeners(): Conecta los campos al preview
2. Event Listeners
Patrón Básico
function initializeEventListeners() {
// Lista de campos que deben actualizar el preview
const fields = [
'enabled',
'showOnMobile',
'bgColor',
'textColor',
'highlightText',
'messageText',
'showLink',
'linkText',
'linkUrl'
];
// Conectar event listeners automáticamente
fields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element) {
// Checkboxes: usar 'change'
if (element.type === 'checkbox') {
element.addEventListener('change', updatePreview);
}
// Otros inputs: usar 'input' para tiempo real
else {
element.addEventListener('input', updatePreview);
}
}
});
// Event listeners específicos
initializeColorPickerListeners();
initializeTextareaCounters();
initializeResetButton();
}
Color Pickers con Display Hex
function initializeColorPickerListeners() {
const colorFields = [
{ input: 'bgColor', display: 'bgColorValue' },
{ input: 'textColor', display: 'textColorValue' },
{ input: 'highlightColor', display: 'highlightColorValue' }
];
colorFields.forEach(({ input, display }) => {
const inputElement = document.getElementById(input);
const displayElement = document.getElementById(display);
if (inputElement && displayElement) {
inputElement.addEventListener('input', function() {
displayElement.textContent = this.value.toUpperCase();
updatePreview();
});
}
});
}
Textareas con Contadores
function initializeTextareaCounters() {
const textareas = [
{ field: 'messageText', counter: 'messageTextCount', progress: 'messageTextProgress', max: 250 },
{ field: 'description', counter: 'descriptionCount', progress: 'descriptionProgress', max: 500 }
];
textareas.forEach(({ field, counter, progress, max }) => {
const textarea = document.getElementById(field);
const counterElement = document.getElementById(counter);
const progressElement = document.getElementById(progress);
if (textarea && counterElement && progressElement) {
textarea.addEventListener('input', function() {
const length = this.value.length;
const percentage = (length / max) * 100;
// Actualizar contador
counterElement.textContent = length;
// Actualizar progress bar
progressElement.style.width = percentage + '%';
progressElement.setAttribute('aria-valuenow', length);
// Cambiar color según uso
if (percentage > 90) {
progressElement.style.backgroundColor = '#dc3545'; // Rojo
} else if (percentage > 75) {
progressElement.style.backgroundColor = '#ffc107'; // Amarillo
} else {
progressElement.style.backgroundColor = '#FF8600'; // Orange
}
updatePreview();
});
}
});
}
Botón de Reset
function initializeResetButton() {
const resetBtn = document.getElementById('resetDefaults');
if (resetBtn) {
resetBtn.addEventListener('click', resetToDefaults);
}
}
3. Función updatePreview()
/**
* Actualiza la vista previa en tiempo real
* REGLA: Solo modificar propiedades que el usuario puede cambiar
*/
function updatePreview() {
const preview = document.getElementById('componentPreview');
if (!preview) return;
// 1. Activar/desactivar componente
const enabled = document.getElementById('enabled').checked;
preview.style.display = enabled ? 'block' : 'none';
// 2. Colores
const bgColor = document.getElementById('bgColor').value;
const textColor = document.getElementById('textColor').value;
preview.style.backgroundColor = bgColor;
preview.style.color = textColor;
// 3. Textos
const highlightText = document.getElementById('highlightText').value;
const messageText = document.getElementById('messageText').value;
const highlightElement = document.getElementById('previewHighlight');
const messageElement = document.getElementById('previewMessage');
if (highlightElement) highlightElement.textContent = highlightText;
if (messageElement) messageElement.textContent = messageText;
// 4. Mostrar/ocultar elementos
const showIcon = document.getElementById('showIcon').checked;
const iconElement = document.getElementById('previewIcon');
if (iconElement) {
iconElement.style.display = showIcon ? 'inline-block' : 'none';
}
// 5. Cambiar clase del icono
const iconClass = document.getElementById('iconClass').value;
if (iconElement && iconClass) {
iconElement.className = iconClass + ' notification-icon';
}
// 6. Link
const showLink = document.getElementById('showLink').checked;
const linkElement = document.getElementById('previewLink');
if (linkElement) {
linkElement.style.display = showLink ? 'inline-block' : 'none';
if (showLink) {
const linkText = document.getElementById('linkText').value;
const linkUrl = document.getElementById('linkUrl').value;
const linkTarget = document.getElementById('linkTarget').value;
linkElement.textContent = linkText;
linkElement.href = linkUrl;
linkElement.target = linkTarget;
}
}
}
4. Guardar Configuración
localStorage (Temporal)
/**
* Guarda la configuración en localStorage
*/
function saveConfig() {
const config = {
enabled: document.getElementById('enabled').checked,
showOnMobile: document.getElementById('showOnMobile').checked,
showOnDesktop: document.getElementById('showOnDesktop').checked,
bgColor: document.getElementById('bgColor').value,
textColor: document.getElementById('textColor').value,
highlightColor: document.getElementById('highlightColor').value,
highlightText: document.getElementById('highlightText').value,
messageText: document.getElementById('messageText').value,
showIcon: document.getElementById('showIcon').checked,
iconClass: document.getElementById('iconClass').value,
showLink: document.getElementById('showLink').checked,
linkText: document.getElementById('linkText').value,
linkUrl: document.getElementById('linkUrl').value,
linkTarget: document.getElementById('linkTarget').value
};
localStorage.setItem('componentConfig', JSON.stringify(config));
console.log('💾 Configuración guardada:', config);
}
Archivo JSON (Persistente)
/**
* Guarda la configuración en archivo JSON
*/
async function saveConfigToFile() {
const config = {
component: '[component-name]', // Nombre del componente en kebab-case
version: '1.0',
lastModified: new Date().toISOString(),
config: {
enabled: document.getElementById('enabled').checked,
bgColor: document.getElementById('bgColor').value,
textColor: document.getElementById('textColor').value,
messageText: document.getElementById('messageText').value,
// ... todos los campos del componente
}
};
try {
const response = await fetch('./config.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config, null, 2)
});
if (response.ok) {
console.log('💾 Configuración guardada exitosamente');
showNotification('Cambios guardados', 'success');
} else {
throw new Error('Error al guardar');
}
} catch (error) {
console.error('❌ Error al guardar:', error);
showNotification('Error al guardar cambios', 'error');
}
}
5. Cargar Configuración
Desde localStorage
/**
* Carga la configuración desde localStorage
*/
function loadConfig() {
const saved = localStorage.getItem('componentConfig');
if (!saved) {
console.log('ℹ️ No hay configuración guardada, usando valores por defecto');
return;
}
const config = JSON.parse(saved);
// Aplicar valores guardados
Object.keys(config).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = config[key];
} else if (element.type === 'color') {
element.value = config[key];
// Actualizar display del hex
const displayElement = document.getElementById(key + 'Value');
if (displayElement) {
displayElement.textContent = config[key].toUpperCase();
}
} else {
element.value = config[key];
}
}
});
console.log('📂 Configuración cargada:', config);
updatePreview();
}
Desde archivo JSON
/**
* Carga la configuración desde archivo JSON
*/
async function loadConfigFromFile() {
try {
const response = await fetch('./config.json');
if (!response.ok) {
throw new Error('Config file not found');
}
const data = await response.json();
const config = data.config;
// Aplicar valores cargados (igual que con localStorage)
Object.keys(config).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = config[key];
} else {
element.value = config[key];
}
}
});
console.log('📂 Configuración cargada desde archivo:', config);
updatePreview();
} catch (error) {
console.log('ℹ️ No se encontró config.json, usando valores por defecto');
resetToDefaults();
}
}
6. Reset a Valores por Defecto
/**
* Restaura los valores por defecto
*/
function resetToDefaults() {
if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
return;
}
// Valores por defecto
const defaults = {
enabled: true,
showOnMobile: true,
showOnDesktop: true,
bgColor: '#0E2337',
textColor: '#ffffff',
highlightColor: '#FF8600',
highlightText: 'Nuevo:',
messageText: 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
showIcon: true,
iconClass: 'bi bi-megaphone-fill',
showLink: true,
linkText: 'Ver Catálogo →',
linkUrl: '/catalogo',
linkTarget: '_self',
fontSize: 'normal'
};
// Aplicar defaults
Object.keys(defaults).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = defaults[key];
} else {
element.value = defaults[key];
}
// Actualizar display de colores
if (element.type === 'color') {
const displayElement = document.getElementById(key + 'Value');
if (displayElement) {
displayElement.textContent = defaults[key].toUpperCase();
}
}
}
});
updatePreview();
saveConfig();
console.log('🔄 Valores por defecto restaurados');
}
7. Validación de Formularios
/**
* Valida los campos del formulario
*/
function validateForm() {
let isValid = true;
const errors = [];
// 1. Validar campo requerido
const messageText = document.getElementById('messageText').value.trim();
if (!messageText) {
errors.push('El mensaje principal es requerido');
isValid = false;
}
// 2. Validar longitud
if (messageText.length > 250) {
errors.push('El mensaje no puede exceder 250 caracteres');
isValid = false;
}
// 3. Validar URL
const linkUrl = document.getElementById('linkUrl').value.trim();
if (linkUrl && !isValidUrl(linkUrl)) {
errors.push('La URL no es válida');
isValid = false;
}
// 4. Validar formato de clase CSS
const iconClass = document.getElementById('iconClass').value.trim();
if (iconClass && !/^[\w\s-]+$/.test(iconClass)) {
errors.push('La clase del icono contiene caracteres inválidos');
isValid = false;
}
if (!isValid) {
alert('⚠️ Errores de validación:\n\n' + errors.join('\n'));
}
return isValid;
}
/**
* Valida si una URL es válida
*/
function isValidUrl(string) {
// Permitir rutas relativas
if (string.startsWith('/')) {
return true;
}
// Validar URLs absolutas
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
8. Sistema de Notificaciones
/**
* Muestra notificación temporal
*/
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 start-50 translate-middle-x mt-3`;
notification.style.zIndex = '9999';
notification.innerHTML = `
<i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Uso
showNotification('Cambios guardados', 'success');
showNotification('Error al guardar cambios', 'error');
9. Auto-save (Opcional)
/**
* Auto-guarda cada vez que cambia un campo
*/
function initializeAutoSave() {
const fields = document.querySelectorAll('input, select, textarea');
fields.forEach(field => {
field.addEventListener('change', function() {
saveConfig();
console.log('💾 Auto-guardado');
});
});
}
// Llamar después de initializeEventListeners()