/** * NovelasVM Portal * Carga el catalogo de juegos y gestiona temas. */ (function () { 'use strict'; const THEME_KEY = 'novelasvm-theme'; const THEMES = ['dark', 'light', 'immersive']; const engineConfig = { renpy: { label: 'Ren\'Py', icon: '' }, 'umineko-ru': { label: 'ONScripter-RU', icon: '' }, unity: { label: 'Unity', icon: '' }, web: { label: 'Web', icon: '' } }; // --- Theme handling ------------------------------------------------------ function getSavedTheme() { const saved = localStorage.getItem(THEME_KEY); if (saved && THEMES.includes(saved)) return saved; if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) { return 'light'; } return 'dark'; } function setTheme(theme) { if (!THEMES.includes(theme)) return; document.documentElement.setAttribute('data-theme', theme); localStorage.setItem(THEME_KEY, theme); updateThemeButtons(theme); } function updateThemeButtons(activeTheme) { document.querySelectorAll('.theme-btn').forEach(btn => { const btnTheme = btn.getAttribute('data-theme-value'); btn.classList.toggle('active', btnTheme === activeTheme); btn.setAttribute('aria-pressed', btnTheme === activeTheme ? 'true' : 'false'); }); } function initThemeSwitcher() { setTheme(getSavedTheme()); document.querySelectorAll('.theme-btn').forEach(btn => { btn.addEventListener('click', () => setTheme(btn.getAttribute('data-theme-value'))); }); } // --- Catalog rendering --------------------------------------------------- function formatDate(isoString) { if (!isoString) return ''; try { const date = new Date(isoString); return date.toLocaleDateString('es-ES', { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) { return ''; } } function getEngineBadge(engine) { const cfg = engineConfig[engine] || engineConfig.web; return `${cfg.icon}${cfg.label}`; } function getCoverImage(game) { if (game.cover) { return `Portada de ${escapeHtml(game.title)}`; } return `
`; } function escapeHtml(text) { if (text === null || text === undefined) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderCard(game, index) { const engine = game.engine || 'web'; const metaParts = []; if (game.version) metaParts.push(`v${escapeHtml(game.version)}`); if (game.author) metaParts.push(escapeHtml(game.author)); const date = formatDate(game.createdAt); if (date) metaParts.push(date); return `
${getCoverImage(game)} ${getEngineBadge(engine)}

${escapeHtml(game.title || game.slug)}

${game.subtitle ? `

${escapeHtml(game.subtitle)}

` : ''} ${game.description ? `

${escapeHtml(game.description)}

` : ''}
${metaParts.join(' ยท ')}
`; } function renderLegend() { const legend = document.getElementById('engineLegend'); if (!legend) return; legend.innerHTML = Object.entries(engineConfig).map(([key, cfg]) => ` ${cfg.label} `).join(''); } function showSkeletons(grid) { grid.innerHTML = Array.from({ length: 6 }).map(() => `
`).join(''); } async function loadCatalog() { const grid = document.getElementById('gamesGrid'); const stats = document.getElementById('stats'); const empty = document.getElementById('emptyState'); if (!grid || !stats || !empty) return; showSkeletons(grid); grid.setAttribute('aria-busy', 'true'); try { const response = await fetch('/games.json', { cache: 'no-store' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const games = Array.isArray(data.games) ? data.games : []; // Ordenar: primero por motor (renpy, unity, web) luego por titulo games.sort((a, b) => { const engineOrder = { renpy: 0, 'umineko-ru': 1, unity: 2, web: 3 }; const ea = engineOrder[a.engine] ?? 99; const eb = engineOrder[b.engine] ?? 99; if (ea !== eb) return ea - eb; return (a.title || a.slug).localeCompare(b.title || b.slug); }); if (games.length === 0) { grid.innerHTML = ''; grid.classList.add('hidden'); empty.classList.remove('hidden'); stats.textContent = 'No hay novelas publicadas'; } else { empty.classList.add('hidden'); grid.classList.remove('hidden'); grid.innerHTML = games.map((game, i) => renderCard(game, i)).join(''); const countText = games.length === 1 ? '1 novela disponible' : `${games.length} novelas disponibles`; stats.textContent = countText; } } catch (err) { console.error('Error cargando catalogo:', err); grid.innerHTML = ''; grid.classList.add('hidden'); empty.classList.remove('hidden'); empty.querySelector('h2').textContent = 'No se pudo cargar el catalogo'; empty.querySelector('p').textContent = 'Revisa que /games.json exista o intenta recargar la pagina.'; stats.textContent = 'Error de carga'; } finally { grid.setAttribute('aria-busy', 'false'); } } // --- Init ---------------------------------------------------------------- document.addEventListener('DOMContentLoaded', () => { initThemeSwitcher(); renderLegend(); loadCatalog(); }); })();