refactor: reorganizar openspec y planificacion con spec recaptcha
- 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>
This commit is contained in:
443
_planificacion/01-design-system/12-PERSISTENCIA-JSON.md
Normal file
443
_planificacion/01-design-system/12-PERSISTENCIA-JSON.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# 💾 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)
|
||||
Reference in New Issue
Block a user