'use strict'; const results = document.querySelector('#results'); const status = document.querySelector('#status'); const widgetRoot = document.querySelector('.widget'); const API_ROOT = '/loteriasyapuestas'; const games = [ ['LNAC', 'Lotería Nacional', '#007ac0', 'LN'], ['LAPR', 'La Primitiva', '#008547', 'LP'], ['EMIL', 'Euromillones', '#001367', 'EM'], ['BONO', 'Bonoloto', '#7a8634', 'BL'], ['ELGR', 'El Gordo de la Primitiva', '#dc3f47', 'GP'], ['LAQU', 'La Quiniela', '#cb1517', 'Q'], ['QGOL', 'Quinigol', '#74bacd', 'QG'], ['LOTU', 'Lototurf', '#d16929', 'LT'], ['QUPL', 'Quíntuple Plus', '#eab842', 'QP'], ['EDMS', 'EuroDreams', '#6b3e98', 'ED'] ].map(([id, name, color, short]) => ({ id, name, color, short })); const dateFormatter = new Intl.DateTimeFormat('es-ES', { day: 'numeric', month: 'long', timeZone: 'Europe/Madrid' }); const historyDateFormatter = new Intl.DateTimeFormat('es-ES', { weekday: 'short', day: '2-digit', month: '2-digit', year: '2-digit', timeZone: 'Europe/Madrid' }); function escapeHtml(value) { return String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); } function decodeEntities(value) { const node = document.createElement('textarea'); node.innerHTML = String(value ?? ''); return node.value; } function dateOf(value) { const found = String(value || '').match(/^(\d{4}-\d{2}-\d{2})/); return found?.[1] || null; } function euro(value) { if (value === null || value === undefined || value === '') return null; const text = String(value).trim(); const number = Number(text.includes(',') ? text.replace(/\./g, '').replace(',', '.') : text); return Number.isFinite(number) ? new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(number) : null; } function unwrap(data) { return Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); } function ymd(days) { const date = new Date(); date.setDate(date.getDate() + days); return date.toISOString().slice(0, 10).replaceAll('-', ''); } function ymdMonthsAgo(months) { const date = new Date(); date.setMonth(date.getMonth() - months); return date.toISOString().slice(0, 10).replaceAll('-', ''); } let resizeFrame = 0; function resize() { if (parent === window || !widgetRoot) return; cancelAnimationFrame(resizeFrame); resizeFrame = requestAnimationFrame(() => { const height = Math.ceil(widgetRoot.getBoundingClientRect().height); if (height > 0) parent.postMessage({ type: 'resultados-loterias:resize', height }, '*'); }); } new ResizeObserver(resize).observe(widgetRoot); resize(); async function request(route, query) { const url = new URL(`${API_ROOT}${route}`, location.origin); Object.entries(query).forEach(([key, value]) => url.searchParams.set(key, value)); const response = await fetch(url, { headers: { accept: 'application/json' } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function records(id, date) { const end = date ? date.replaceAll('-', '') : ymd(0); const start = date ? end : ymdMonthsAgo(3); const payload = await request('/servicios/buscadorSorteos', { game_id: id, celebrados: 'true', fechaInicioInclusiva: start, fechaFinInclusiva: end }); return unwrap(payload).filter((row) => { const rowDate = dateOf(row.fecha_sorteo || row.fechaSorteo || row.fecha); return rowDate && (date || rowDate.replaceAll('-', '') >= start); }).sort((a, b) => String(b.fecha_sorteo || b.fechaSorteo || b.fecha).localeCompare(String(a.fecha_sorteo || a.fechaSorteo || a.fecha))); } async function provisional(id, next) { if (!['LAQU', 'QGOL'].includes(id)) return null; const payload = await request('/servicios/fechav3', { game_id: id, fecha_sorteo: ymd(0) }); return unwrap(payload)[0] || null; } function combination(row) { const value = row?.combinacion ?? row?.combination ?? row?.resultado; if (typeof value === 'string') return value.split(/[\s,]+/).map((part) => part.trim()).filter(Boolean); if (Array.isArray(value)) return value.map(String); if (Array.isArray(row?.partidos)) return row.partidos.map((match) => match.signo || match.resultado || '-'); return [row?.primerPremio?.decimo && `1º ${row.primerPremio.decimo}`, row?.segundoPremio?.decimo && `2º ${row.segundoPremio.decimo}`].filter(Boolean); } function nestedResult(row, keys) { for (const key of keys) { const value = row?.[key]; const result = typeof value === 'object' && value !== null ? (value.combinacion ?? value.codigo ?? value.resultado ?? value.numero) : value; if (result !== null && result !== undefined && String(result).trim() && String(result).trim() !== '-') return String(result).trim(); } return null; } function nationalPrizeRows(row) { const rows = Array.isArray(row.escrutinio) ? row.escrutinio.map((item) => ({ label: decodeEntities(item.tipo || item.categoria || 'Premio'), value: euro(item.premio) || item.premio || 'Pendiente' })) : []; const additions = [ ['extraccionesDeCincoCifras', 'Extracción de 5 cifras'], ['extraccionesDeCuatroCifras', 'Extracción de 4 cifras'], ['extraccionesDeTresCifras', 'Extracción de 3 cifras'], ['extraccionesDeDosCifras', 'Extracción de 2 cifras'] ]; additions.forEach(([key, label]) => { const first = Array.isArray(row[key]) ? row[key][0] : null; if (first?.prize !== undefined) rows.push({ label, value: euro(Number(first.prize) / 100) || 'Pendiente' }); }); return rows; } function gameLogo(game) { return `${escapeHtml(game.name)}`; } function resultMarkup(game) { return game.balls.map((item, index) => { const extra = item.match(/^(C|R)\(([^)]+)\)$/i); if (extra) { const label = game.id === 'EDMS' && extra[1] === 'C' ? 'Sueño' : extra[1].toUpperCase(); return `${label}${escapeHtml(extra[2])}`; } const isQuiniela = game.id === 'LAQU'; if (isQuiniela && index === game.balls.length - 1) return `${escapeHtml(item)}`; const isStar = game.id === 'EMIL' && index >= 5 && index <= 6; const isBonus = /^(M-\d+|JOKER|\d{7,})$/i.test(item); const colorClass = ['LAPR', 'BONO', 'EMIL', 'ELGR', 'LAQU', 'EDMS'].includes(game.id) && !isQuiniela && !isStar && !isBonus ? ` ball--colored` : ''; const typeClass = game.id === 'LNAC' ? ' ball--rectangular' : (isQuiniela ? ' quiniela-sign' : (isStar ? ' ball--star' : (isBonus ? ' ball--text' : colorClass))); return `${isStar ? '★ ' : ''}${escapeHtml(item)}`; }).join(''); } function card(game, row = {}, next = {}) { const date = dateOf(row.fecha_sorteo || row.fechaSorteo || row.fecha); const parts = date ? dateFormatter.format(new Date(`${date}T12:00:00+02:00`)).split(' de ') : ['—', 'SIN RESULTADOS']; const balls = combination(row).filter((item) => game.id === 'LAQU' || item !== '-'); const prizeRows = game.id === 'LNAC' ? nationalPrizeRows(row) : (Array.isArray(row.escrutinio) ? row.escrutinio.map((item) => ({ label: decodeEntities(item.tipo || item.categoria || 'Premio'), value: euro(item.premio) || item.premio || 'Pendiente' })) : []); const refunds = game.id === 'LNAC' && Array.isArray(row.reintegros) ? row.reintegros.map((item) => item.decimo).filter(Boolean) : []; const historicFirstPrize = Number(row.primer_premio?.prize ?? row.primer_premio); const seriesPrizeValue = game.id === 'LNAC' ? (next.primer_premio ?? (Number.isFinite(historicFirstPrize) ? historicFirstPrize / 10 : null)) : null; const joker = game.id === 'LAPR' ? nestedResult(row, ['joker']) : null; const elMillon = game.id === 'EMIL' ? nestedResult(row, ['millon', 'elMillon', 'el_millon', 'codigoMillon', 'codigo_millon']) : null; // The footer describes the next draw, not the already-played draw's historical jackpot. const jackpot = game.id === 'LNAC' ? null : (euro(next.premio_bote) ?? euro(0)); return { ...game, date, day: parts[0], month: parts[1].toUpperCase(), weekday: row.dia_semana || '', balls, refunds, seriesPrize: euro(seriesPrizeValue), jackpot, joker, elMillon, prizes: prizeRows, matches: row.partidos || [] }; } function template(game) { const balls = game.balls.length ? resultMarkup(game) : 'Resultado pendiente de publicación'; const refunds = game.refunds?.length ? `R${game.refunds.map((item) => `${escapeHtml(item)}`).join('')}` : ''; const extras = `${game.joker ? `JOKER${escapeHtml(game.joker)}` : ''}${game.elMillon ? `EL MILLÓN${escapeHtml(game.elMillon)}` : ''}`; const footer = game.seriesPrize ? `` : (game.id === 'EDMS' ? `` : (game.jackpot !== null ? `` : '')); return `
${gameLogo(game)}
${escapeHtml(game.weekday)}${game.day}${game.month}
${balls}${refunds}${extras}
${footer}
`; } function prizes(game) { if (!game.prizes.length) return ''; return `${game.prizes.map((item) => ``).join('')}
CategoríaPremio
${escapeHtml(item.label)}${escapeHtml(item.value)}
`; } function asElement(html) { const node = document.createElement('template'); node.innerHTML = html; return node.content.firstElementChild; } function renderResults(items) { const columns = [items.filter((_, index) => index % 2 === 0), items.filter((_, index) => index % 2 === 1)]; results.innerHTML = columns.map((column) => `
${column.map(template).join('')}
`).join(''); } results.addEventListener('click', async (event) => { const button = event.target.closest('[data-action]'); if (!button) return; const current = button.closest('.card'); const kind = button.dataset.action; const panel = current.querySelector(`[data-details="${kind}"]`); const opening = panel.hidden; current.querySelectorAll('[data-details]').forEach((item) => { item.hidden = true; }); if (!opening) return; if (kind === 'prizes') { const game = window.widgetGames.find((item) => item.id === current.dataset.game); if (!game.prizes.length) return; panel.innerHTML = prizes(game); } panel.hidden = false; if (kind === 'prizes') { /* El contenido ya se ha preparado arriba. */ } else if (!panel.dataset.loaded) { panel.innerHTML = '

Cargando fechas…

'; try { const dates = await records(current.dataset.game); panel.innerHTML = `
${dates.map((item) => { const value = dateOf(item.fecha_sorteo || item.fechaSorteo || item.fecha); const label = historyDateFormatter.format(new Date(`${value}T12:00:00+02:00`)).replace('.', '').toUpperCase(); return ``; }).join('')}
`; panel.dataset.loaded = 'true'; panel.querySelectorAll('.history__date').forEach((dateButton) => dateButton.addEventListener('click', async () => { const selected = dateButton.value; panel.querySelectorAll('.history__date').forEach((item) => { item.disabled = true; }); try { const row = (await records(current.dataset.game, selected))[0]; const game = games.find((item) => item.id === current.dataset.game); const replacement = card(game, row); window.widgetGames = window.widgetGames.map((item) => item.id === game.id ? replacement : item); current.replaceWith(asElement(template(replacement))); resize(); } finally { panel.querySelectorAll('.history__date').forEach((item) => { item.disabled = false; }); } })); } catch { panel.innerHTML = '

No se pudo cargar el histórico.

'; } } resize(); }); (async () => { try { const upcoming = new Map(unwrap(await request('/servicios/proximosv3', { game_id: 'TODOS', num: '1' })).map((item) => [item.game_id, item])); window.widgetGames = await Promise.all(games.map(async (game) => { const next = upcoming.get(game.id); try { const current = await provisional(game.id, next); if (current) return card(game, current, next); return card(game, (await records(game.id))[0], next); } catch { try { return card(game, (await records(game.id))[0], next); } catch { return card(game, {}, next); } } })); renderResults(window.widgetGames); status.hidden = true; resize(); } catch { status.textContent = 'No se pueden mostrar los resultados en este momento. Vuelve a intentarlo en unos minutos.'; } })();