- 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>
444 lines
13 KiB
Markdown
444 lines
13 KiB
Markdown
# 💾 PERSISTENCIA EN ARCHIVOS JSON
|
||
|
||
## Estructura del Archivo config.json
|
||
|
||
Cada componente DEBE tener su archivo `config.json` en su carpeta:
|
||
|
||
```json
|
||
{
|
||
"component": "[component-name]",
|
||
"version": "1.0",
|
||
"lastModified": "2025-01-15T10:30:00Z",
|
||
"config": {
|
||
"enabled": true,
|
||
"showOnMobile": true,
|
||
"showOnDesktop": true,
|
||
"bgColor": "#0E2337",
|
||
"textColor": "#ffffff",
|
||
"highlightColor": "#FF8600",
|
||
"linkHoverColor": "#FF6B35",
|
||
"fontSize": "normal",
|
||
"showIcon": true,
|
||
"iconClass": "bi bi-icon-name",
|
||
"highlightText": "Texto destacado",
|
||
"messageText": "Mensaje principal del componente",
|
||
"showLink": true,
|
||
"linkText": "Texto del enlace",
|
||
"linkUrl": "/ruta",
|
||
"linkTarget": "_self"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Campos del Metadata
|
||
|
||
| Campo | Tipo | Descripción |
|
||
|-------|------|-------------|
|
||
| `component` | string | Nombre del componente (kebab-case) |
|
||
| `version` | string | Versión del config (semver) |
|
||
| `lastModified` | string | Timestamp ISO 8601 de última modificación |
|
||
| `config` | object | Objeto con la configuración del componente |
|
||
|
||
---
|
||
|
||
## Funciones de Persistencia
|
||
|
||
### Guardar Configuración
|
||
|
||
```javascript
|
||
/**
|
||
* Guarda la configuración en archivo JSON
|
||
*/
|
||
async function saveConfig() {
|
||
const config = {
|
||
component: '[component-name]', // Nombre del componente en kebab-case
|
||
version: '1.0',
|
||
lastModified: new Date().toISOString(),
|
||
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,
|
||
linkHoverColor: document.getElementById('linkHoverColor').value,
|
||
fontSize: document.getElementById('fontSize').value,
|
||
showIcon: document.getElementById('showIcon').checked,
|
||
iconClass: document.getElementById('iconClass').value,
|
||
highlightText: document.getElementById('highlightText').value,
|
||
messageText: document.getElementById('messageText').value,
|
||
showLink: document.getElementById('showLink').checked,
|
||
linkText: document.getElementById('linkText').value,
|
||
linkUrl: document.getElementById('linkUrl').value,
|
||
linkTarget: document.getElementById('linkTarget').value
|
||
}
|
||
};
|
||
|
||
try {
|
||
// Guardar en archivo JSON
|
||
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');
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Cargar Configuración
|
||
|
||
```javascript
|
||
/**
|
||
* Carga la configuración desde archivo JSON
|
||
*/
|
||
async function loadConfig() {
|
||
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;
|
||
|
||
// Validar estructura
|
||
if (!validateConfigStructure(data)) {
|
||
throw new Error('Invalid config structure');
|
||
}
|
||
|
||
// Aplicar valores cargados
|
||
if (config.enabled !== undefined) {
|
||
document.getElementById('enabled').checked = config.enabled;
|
||
}
|
||
if (config.showOnMobile !== undefined) {
|
||
document.getElementById('showOnMobile').checked = config.showOnMobile;
|
||
}
|
||
if (config.showOnDesktop !== undefined) {
|
||
document.getElementById('showOnDesktop').checked = config.showOnDesktop;
|
||
}
|
||
if (config.bgColor) {
|
||
document.getElementById('bgColor').value = config.bgColor;
|
||
document.getElementById('bgColorValue').textContent = config.bgColor.toUpperCase();
|
||
}
|
||
if (config.textColor) {
|
||
document.getElementById('textColor').value = config.textColor;
|
||
document.getElementById('textColorValue').textContent = config.textColor.toUpperCase();
|
||
}
|
||
if (config.highlightColor) {
|
||
document.getElementById('highlightColor').value = config.highlightColor;
|
||
document.getElementById('highlightColorValue').textContent = config.highlightColor.toUpperCase();
|
||
}
|
||
if (config.fontSize) {
|
||
document.getElementById('fontSize').value = config.fontSize;
|
||
}
|
||
if (config.showIcon !== undefined) {
|
||
document.getElementById('showIcon').checked = config.showIcon;
|
||
}
|
||
if (config.iconClass) {
|
||
document.getElementById('iconClass').value = config.iconClass;
|
||
}
|
||
if (config.highlightText) {
|
||
document.getElementById('highlightText').value = config.highlightText;
|
||
}
|
||
if (config.messageText) {
|
||
document.getElementById('messageText').value = config.messageText;
|
||
// Actualizar contador si existe
|
||
const counter = document.getElementById('messageTextCount');
|
||
if (counter) {
|
||
counter.textContent = config.messageText.length;
|
||
}
|
||
}
|
||
if (config.showLink !== undefined) {
|
||
document.getElementById('showLink').checked = config.showLink;
|
||
}
|
||
if (config.linkText) {
|
||
document.getElementById('linkText').value = config.linkText;
|
||
}
|
||
if (config.linkUrl) {
|
||
document.getElementById('linkUrl').value = config.linkUrl;
|
||
}
|
||
if (config.linkTarget) {
|
||
document.getElementById('linkTarget').value = config.linkTarget;
|
||
}
|
||
|
||
console.log('📂 Configuración cargada:', config);
|
||
updatePreview();
|
||
|
||
} catch (error) {
|
||
console.log('ℹ️ No se encontró config.json, usando valores por defecto');
|
||
resetToDefaults();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Validación de JSON
|
||
|
||
```javascript
|
||
/**
|
||
* Valida la estructura del archivo config.json
|
||
*/
|
||
function validateConfigStructure(data) {
|
||
const required = ['component', 'version', 'config'];
|
||
|
||
// Verificar campos requeridos
|
||
for (const field of required) {
|
||
if (!data.hasOwnProperty(field)) {
|
||
console.error(`❌ Campo requerido faltante: ${field}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Verificar que config sea un objeto
|
||
if (typeof data.config !== 'object' || data.config === null) {
|
||
console.error('❌ El campo "config" debe ser un objeto');
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Sistema de Notificaciones
|
||
|
||
```javascript
|
||
/**
|
||
* 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');
|
||
showNotification('Configuración restaurada', 'success');
|
||
```
|
||
|
||
---
|
||
|
||
## Migración de localStorage a JSON
|
||
|
||
Si tienes datos en localStorage que quieres migrar a JSON:
|
||
|
||
```javascript
|
||
/**
|
||
* Migra la configuración de localStorage a archivo JSON
|
||
*/
|
||
async function migrateFromLocalStorage() {
|
||
const saved = localStorage.getItem('topBarConfig');
|
||
|
||
if (!saved) {
|
||
console.log('ℹ️ No hay datos en localStorage para migrar');
|
||
return;
|
||
}
|
||
|
||
const config = JSON.parse(saved);
|
||
|
||
// Crear estructura de config.json
|
||
const jsonConfig = {
|
||
component: '[component-name]', // Nombre del componente en kebab-case
|
||
version: '1.0',
|
||
lastModified: new Date().toISOString(),
|
||
config: config
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('./config.json', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(jsonConfig, null, 2)
|
||
});
|
||
|
||
if (response.ok) {
|
||
console.log('✅ Migración exitosa de localStorage a JSON');
|
||
// Opcional: limpiar localStorage
|
||
localStorage.removeItem('topBarConfig');
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error al migrar:', error);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Manejo de Errores
|
||
|
||
```javascript
|
||
/**
|
||
* Guarda con manejo completo de errores
|
||
*/
|
||
async function saveConfigWithErrorHandling() {
|
||
// 1. Validar antes de guardar
|
||
if (!validateForm()) {
|
||
return;
|
||
}
|
||
|
||
// 2. Preparar datos
|
||
const config = buildConfigObject();
|
||
|
||
// 3. Intentar guardar
|
||
try {
|
||
const response = await fetch('./config.json', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(config, null, 2)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
console.log('💾 Configuración guardada exitosamente');
|
||
showNotification('Cambios guardados correctamente', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error al guardar:', error);
|
||
|
||
// Notificar al usuario
|
||
showNotification('Error al guardar. Verifica la conexión.', 'error');
|
||
|
||
// Fallback: guardar en localStorage temporalmente
|
||
localStorage.setItem('topBarConfig_backup', JSON.stringify(config));
|
||
console.log('💾 Backup guardado en localStorage');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Construye el objeto de configuración
|
||
*/
|
||
function buildConfigObject() {
|
||
return {
|
||
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,
|
||
// ... resto de campos
|
||
}
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Backup y Restore
|
||
|
||
### Crear Backup
|
||
|
||
```javascript
|
||
/**
|
||
* Crea un backup de la configuración actual
|
||
*/
|
||
async function createBackup() {
|
||
try {
|
||
const response = await fetch('./config.json');
|
||
const config = await response.json();
|
||
|
||
// Agregar timestamp al nombre del backup
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const backupName = `config_backup_${timestamp}.json`;
|
||
|
||
// Descargar como archivo
|
||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = backupName;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
showNotification('Backup creado exitosamente', 'success');
|
||
} catch (error) {
|
||
console.error('❌ Error al crear backup:', error);
|
||
showNotification('Error al crear backup', 'error');
|
||
}
|
||
}
|
||
```
|
||
|
||
### Restaurar Backup
|
||
|
||
```javascript
|
||
/**
|
||
* Restaura un backup
|
||
*/
|
||
function restoreBackup(file) {
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = async function(e) {
|
||
try {
|
||
const config = JSON.parse(e.target.result);
|
||
|
||
// Validar estructura
|
||
if (!validateConfigStructure(config)) {
|
||
throw new Error('Invalid backup structure');
|
||
}
|
||
|
||
// Aplicar configuración
|
||
const data = config.config;
|
||
Object.keys(data).forEach(key => {
|
||
const element = document.getElementById(key);
|
||
if (element) {
|
||
if (element.type === 'checkbox') {
|
||
element.checked = data[key];
|
||
} else {
|
||
element.value = data[key];
|
||
}
|
||
}
|
||
});
|
||
|
||
updatePreview();
|
||
showNotification('Backup restaurado exitosamente', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error al restaurar backup:', error);
|
||
showNotification('Error al restaurar backup', 'error');
|
||
}
|
||
};
|
||
|
||
reader.readAsText(file);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Volver al Índice
|
||
|
||
[← Volver al README](README.md)
|