VeriSource Hub — Refactorización sin SPA
Guía completa para transformar VeriSource Hub de una Single Page Application (React 18 + Vite) a un sitio estático con HTML/CSS/JS vanilla, manteniendo TODA la funcionalidad existente.
Cuándo Usar Este Skill
- •Refactorizar VeriSource Hub eliminando React y dependencias SPA
- •Convertir componentes JSX a HTML/JS vanilla
- •Migrar de React Context a un sistema de estado vanilla
- •Eliminar Vite como bundler y usar Tailwind CSS standalone
- •Crear una versión estática desplegable sin build de React
- •Optimizar rendimiento eliminando overhead de framework
Arquitectura Actual (SPA React)
Stack Actual
| Tecnología | Uso | Se mantiene |
|---|---|---|
| React 18 | UI Framework (SPA) | NO — se elimina |
| ReactDOM | Rendering | NO — se elimina |
| Vite | Bundler/Dev Server | PARCIAL — reemplazar por servidor estático |
| Tailwind CSS | Estilos | SÍ — usar CLI standalone |
| Lucide React | Iconos | NO — reemplazar por Lucide vanilla/SVG inline |
| PostCSS + Autoprefixer | CSS processing | SÍ — integrado con Tailwind CLI |
| ESLint | Linting | OPCIONAL — adaptar a JS vanilla |
Estructura de Archivos Actual
src/
├── App.jsx # Componente monolítico (1238 líneas) — TODO el UI
├── main.jsx # Entry point React con LanguageProvider
├── index.css # Estilos globales + Tailwind directives
├── components/
│ ├── ExportTools.jsx # Exportar favoritos, compartir config
│ └── SearchHistory.jsx # Vista historial de búsquedas
├── data/
│ └── osint-data.jsx # Catálogo OSINT (8 categorías, 56+ herramientas)
├── i18n/
│ ├── LanguageContext.jsx # React Context para idiomas
│ └── translations.js # Traducciones ES/EN completas
└── utils/
├── hooks.js # Custom hooks: useTheme, useFavorites, useSearchHistory, etc.
├── preload.js # Estrategias de precarga (idle, hover, delay)
└── theme.js # createTheme, buttonVariants, storage utils, debounce
Funcionalidades a Preservar
- •Búsqueda en tiempo real — Filtrar herramientas por nombre, descripción, tags, categoría
- •Categorías — 8 categorías + vista "Todo" con sidebar de navegación
- •Favoritos — Toggle favoritos con persistencia localStorage
- •Historial de búsquedas — Últimas 20 búsquedas con timestamp y conteo de resultados
- •Tema oscuro/claro — Toggle con persistencia, detección sistema, clase
darken<html> - •Internacionalización — ES/EN con detección de navegador y persistencia
- •IA Gemini — Plan de investigación y generador de dorks (textarea → API → resultado)
- •Atajos de teclado — Ctrl+K (buscar), Ctrl+Shift+F (favoritos), Ctrl+Shift+H (historial), Ctrl+Shift+D (tema), Escape (cerrar)
- •Exportar datos — JSON de favoritos, compartir via Web Share API
- •Responsive — Sidebar colapsable en móvil, grid adaptivo
- •Tags por herramienta — Color mapping para 80+ tags con estilos glassmorphism
- •Animaciones — fadeIn, slideUp, pulseGlow, float, card-hover
Datos Clave
- •8 categorías: searchEngines, socialMedia, personSearch, imageMapping, corporateTech, transportLive, artificialIntelligence, verificationBlogs
- •56+ herramientas OSINT con: name, url, descKey (clave i18n), tags[]
- •2 idiomas: Español (por defecto) e Inglés
- •API Gemini:
gemini-2.5-flash-preview-09-2025para AI features
Prerequisitos
- •Node.js 18+ (solo para Tailwind CLI durante desarrollo)
- •Un servidor HTTP simple (Live Server,
npx serve, Pythonhttp.server) - •API key de Google Gemini (opcional, para funciones IA)
Flujo de Refactorización Paso a Paso
Paso 1: Crear estructura de archivos estática
/ (raíz del proyecto) ├── index.html # HTML principal con todo el markup ├── css/ │ ├── input.css # Tailwind directives + custom CSS │ └── output.css # CSS compilado por Tailwind CLI ├── js/ │ ├── app.js # Lógica principal y orquestación │ ├── data.js # Catálogo OSINT (export como objeto JS) │ ├── i18n.js # Sistema de traducciones vanilla │ ├── theme.js # Gestión de tema dark/light │ ├── search.js # Lógica de búsqueda y filtrado │ ├── favorites.js # Gestión de favoritos │ ├── history.js # Historial de búsquedas │ ├── ai.js # Integración Gemini API │ ├── keyboard.js # Atajos de teclado │ └── storage.js # Utilidades localStorage ├── icons/ # SVGs de Lucide descargados ├── public/ │ └── favicon.svg ├── tailwind.config.js # Configuración Tailwind (adaptar content paths) └── package.json # Solo scripts de Tailwind CLI
Paso 2: Migrar Datos — ver data-catalog.md
Convertir osint-data.jsx de exports JSX a objeto JavaScript puro:
- •Eliminar imports de Lucide React
- •Reemplazar
icon: <Component />poricon: "icon-name"(string) - •Resolver SVGs por nombre en runtime
Paso 3: Migrar Sistema i18n — ver translations-reference.md
Reemplazar React Context por módulo vanilla:
- •Función
t(key)con dot-notation:t('categories.searchEngines') - •Detección de idioma del navegador
- •Persistencia en localStorage
- •Fallback automático a inglés
Paso 4: Migrar Sistema de Tema
Convertir de useState + useEffect a vanilla:
- •Toggle clase
darkendocument.documentElement - •localStorage para persistencia
- •
matchMedia('(prefers-color-scheme: dark)')para detección del sistema
Paso 5: Migrar Búsqueda y Filtrado
Convertir useMemo de React a funciones puras:
- •Debounce manual con
setTimeout - •
renderTools(filteredData)que actualiza el DOM - •Event listener en input con debounce
Paso 6: Generar HTML estático
Ver refactoring-plan.md para el HTML completo con:
- •Header sticky con controles
- •Sidebar con categorías
- •Grid de herramientas
- •Modal de AI
- •Vistas de favoritos e historial
Paso 7: Reemplazar Iconos Lucide
Opciones:
- •Lucide vanilla:
npm install lucidey usarcreateIcons() - •SVGs inline: Copiar SVGs directamente en el HTML
- •Sprite SVG: Un archivo sprite con
<use xlink:href="#icon-name">
Recomendado: Lucide vanilla con createIcons() para menor mantenimiento.
Paso 8: Configurar Tailwind CSS Standalone
# package.json mínimo
{
"scripts": {
"dev": "npx tailwindcss -i ./css/input.css -o ./css/output.css --watch",
"build": "npx tailwindcss -i ./css/input.css -o ./css/output.css --minify"
},
"devDependencies": {
"tailwindcss": "^3.3.3",
"autoprefixer": "^10.4.14"
}
}
Adaptar tailwind.config.js:
content: [ "./index.html", "./js/**/*.js", ]
Mapeo de Componentes React → Vanilla
| React (Actual) | Vanilla (Nuevo) | Notas |
|---|---|---|
useState(initialValue) | Variable + función setState() que re-renderiza | Usar closure o módulo |
useEffect(() => {}, [deps]) | addEventListener / DOMContentLoaded | Sin cleanup automático |
useMemo(() => {}, [deps]) | Función pura llamada según necesidad | Cache manual si es costoso |
useCallback(fn, [deps]) | Función normal (no hay re-renders) | Innecesario en vanilla |
useContext(LanguageContext) | Módulo singleton i18n.t('key') | Import directo |
JSX <Component prop={x} /> | document.createElement / template literals | Preferir template literals |
className={...} | element.className = ... | Mismo Tailwind |
onClick={handler} | element.addEventListener('click', handler) | O atributo onclick |
onChange={(e) => ...} | input.addEventListener('input', handler) | Usar input no change |
| React key en listas | No necesario | DOM vanilla no lo requiere |
Patrón de Renderizado Recomendado
// Patrón: Render functions con template literals
function renderToolCard(item, category, isFavorite) {
return `
<div class="group relative p-6 rounded-2xl border transition-all duration-300 card-hover
${isDarkMode
? 'bg-slate-900/60 border-slate-800/50 hover:bg-slate-800/60'
: 'bg-white/90 border-slate-200 hover:bg-slate-50/90 shadow-sm'
} hover:border-VeriSource-500/40 hover:shadow-xl"
data-url="${item.url}" data-desc-key="${item.descKey}">
<div class="flex justify-between items-start mb-4">
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-lg truncate pr-4">${item.name}</h3>
<div class="flex items-center gap-2 mt-2">
${item.tags.slice(0, 2).map(tag =>
`<span class="px-2 py-1 rounded-md text-xs font-medium ${getTagColor(tag)}">${tag}</span>`
).join('')}
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<button onclick="toggleFavorite('${item.url}')" class="p-2 rounded-lg transition-all hover:scale-110">
${isFavorite ? '★' : '☆'}
</button>
<a href="${item.url}" target="_blank" rel="noopener noreferrer" class="p-2 rounded-lg">
<!-- external-link SVG -->
</a>
</div>
</div>
<p class="text-sm line-clamp-2">${t('tools.' + item.descKey)}</p>
</div>
`;
}
// Render completo
function renderTools() {
const filtered = filterData();
const container = document.getElementById('tools-grid');
container.innerHTML = filtered.map((cat, i) => `
<section class="animate-slide-in-from-bottom" style="animation-delay: ${i * 100}ms">
<div class="flex items-center gap-4 mb-8">
<div class="p-3 rounded-xl bg-gradient-to-r ${cat.color} shadow-lg">
${getIconSVG(cat.icon)}
</div>
<h2 class="text-2xl font-bold">${t('categories.' + cat.category)}</h2>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
${cat.items.map(item => renderToolCard(item, cat, isFavorite(item))).join('')}
</div>
</section>
`).join('');
}
Troubleshooting
| Problema | Solución |
|---|---|
| Tailwind no genera clases dinámicas | Usar safelist en config o clases completas (no interpolar) |
| SVGs de Lucide no aparecen | Verificar que createIcons() se ejecuta después de cada render |
| localStorage no persiste | Verificar que los valores se serializan/deserializan con JSON |
| Búsqueda lenta con muchos items | Implementar debounce de 300ms y cache de resultados |
| Tema parpadea al cargar (FOUC) | Ejecutar script de tema en <head> antes del CSS |
| Animaciones no funcionan | Asegurar que las clases de animación están en safelist |
| i18n no detecta idioma | Verificar navigator.language y fallback chain |