Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/site-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ jobs:
node --check tests/consent-coc.cjs
- name: Check consent persistence and Code of Conduct version
run: |
node --test tests/consent-coc.cjs
node --test tests/voluntariado-page.cjs
node --test tests/*.cjs
- name: Check whitespace errors
run: git diff --check HEAD^ HEAD
8 changes: 5 additions & 3 deletions 2026/agenda.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agenda - PyCon Panamá 2026</title>
<meta name="description" content="La agenda estará disponible el lunes 28 de septiembre de 2026.">
<meta name="description" content="La agenda estará disponible el viernes 2 de octubre de 2026.">
<meta name="theme-color" content="#2483f2">
<meta property="og:type" content="website">
<meta property="og:image" content="https://pycon.pa/2026/img/PyConPet.jpg">
<meta property="og:locale" content="es_PA">
<meta property="og:site_name" content="PyCon Panamá">
<meta property="og:title" content="Agenda de PyCon Panamá 2026">
<meta property="og:description" content="La agenda estará disponible el lunes 28 de septiembre de 2026.">
<meta property="og:description" content="La agenda estará disponible el viernes 2 de octubre de 2026.">
<meta property="og:url" content="https://pycon.pa/2026/agenda.html">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://pycon.pa/2026/img/PyConPet.jpg">
Expand Down Expand Up @@ -75,7 +75,8 @@
<div class="container">
<header class="section-header">
<h1>Agenda de PyCon Panamá 2026</h1>
<p class="section-text">La agenda estará disponible el lunes 28 de septiembre de 2026.</p>
<p class="section-text">La agenda estará disponible el <time id="agenda-publication-date" datetime="2026-10-02">viernes 2 de octubre de 2026</time>.</p>
<p id="agenda-countdown" class="section-text agenda-countdown" aria-live="off">La hora de publicación está por confirmar.</p>
</header>
</div>
</section>
Expand All @@ -96,6 +97,7 @@ <h1>Agenda de PyCon Panamá 2026</h1>
</footer>

<script src="js/main.js"></script>
<script src="js/agenda-countdown.js"></script>
</body>

</html>
8 changes: 8 additions & 0 deletions 2026/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1187,3 +1187,11 @@ a.sponsor-card:focus-visible {
.sponsor-empty > * + * {
margin-top: var(--space-3);
}

/* Reserva dos líneas en móvil para todos los estados de la cuenta regresiva. */
.agenda-countdown {
min-height: 3em;
line-height: 1.5;
font-size: 0.95rem;
font-variant-numeric: tabular-nums;
}
40 changes: 40 additions & 0 deletions 2026/js/agenda-countdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/** Días calendario en Panamá; no representa una hora de publicación. */
(function () {
'use strict';

const panamaDate = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Panama', year: 'numeric', month: '2-digit', day: '2-digit'
});

function countdownMessage(publicationDate, now = new Date()) {
const [year, month, day] = publicationDate.split('-').map(Number);
const parts = Object.fromEntries(panamaDate.formatToParts(now).map(part => [part.type, part.value]));
// UTC sirve solo para restar fechas civiles, no fija una hora del evento.
const today = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day));
const remaining = (Date.UTC(year, month - 1, day) - today) / 86400000;
if (remaining > 1) return `Faltan ${remaining} días para publicar la agenda`;
if (remaining === 1) return 'Falta 1 día para publicar la agenda';
if (remaining === 0) return 'La agenda se publica hoy';
return 'Agenda pendiente de publicación';
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = { countdownMessage };
}

if (typeof document === 'undefined') return;
const countdown = document.getElementById('agenda-countdown');
const date = document.getElementById('agenda-publication-date');
if (!countdown || !date) return;

function update() {
const message = countdownMessage(date.getAttribute('datetime'));
if (countdown.textContent !== message) countdown.textContent = message;
}
update();
// Cubre pestañas abiertas durante el cambio de día, sin anuncios en vivo.
window.setInterval(update, 60000);
document.addEventListener('visibilitychange', function () {
if (!document.hidden) update();
});
})();
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ Para consultas sobre PyCon Panamá, escribe a [pyconpanama@gmail.com](mailto:pyc

## Publicación de la agenda 2026 — issue #118

Última actualización: 8 de septiembre de 2026.
Última actualización: 27 de septiembre de 2026.

La agenda permanece en espera hasta el lunes 28 de septiembre de 2026. No hay
La agenda permanece en espera hasta el viernes 2 de octubre de 2026. No hay
hora definida ni publicación automática: organización debe confirmar el contenido
aprobado y el momento de publicación.

Expand All @@ -158,6 +158,8 @@ Para preparar la restitución:
horarios y ponentes por la programación aprobada por organización.
2. Editar la página actual conservando su navegación y metadatos vigentes.
Actualizar la descripción y el contenido principal con la agenda aprobada.
Retirar el aviso de publicación, el elemento `agenda-countdown` y la carga
de `js/agenda-countdown.js` al publicar la programación definitiva.
3. Restituir el enlace Agenda en los menús de todas las páginas de `2026/`,
incluido `voluntariado/index.html` (ruta `../agenda.html`); la tarjeta de
inicio; y los botones de `about.html` y `sedes.html`. Consultar el diff del
Expand All @@ -169,7 +171,7 @@ Para preparar la restitución:
`git diff --check`. Revisar el preview en móvil y escritorio, el menú,
los enlaces restituidos y el acceso directo a `/2026/agenda.html`.
6. Integrar el PR revisado cuando organización autorice la publicación el
28 de septiembre. Verificar el despliegue de Netlify y repetir la navegación
2 de octubre de 2026. Verificar el despliegue de Netlify y repetir la navegación
y el acceso directo en producción antes de dar la restitución por terminada.

El aviso se mantiene si falta la aprobación; no se elimina automáticamente por
Expand Down
58 changes: 58 additions & 0 deletions tests/agenda-countdown.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const { countdownMessage } = require('../2026/js/agenda-countdown.js');
const html = fs.readFileSync('2026/agenda.html', 'utf8');
const publicationDate = html.match(/id="agenda-publication-date" datetime="([^"]+)"/)[1];

for (const [instant, expected] of [
['2026-09-27T17:00:00Z', 'Faltan 5 días para publicar la agenda'],
['2026-10-01T04:59:59Z', 'Faltan 2 días para publicar la agenda'],
['2026-10-01T05:00:00Z', 'Falta 1 día para publicar la agenda'],
['2026-10-02T04:59:59Z', 'Falta 1 día para publicar la agenda'],
['2026-10-02T05:00:00Z', 'La agenda se publica hoy'],
['2026-10-03T04:59:59Z', 'La agenda se publica hoy'],
['2026-10-03T05:00:00Z', 'Agenda pendiente de publicación'],
['2027-01-01T12:00:00Z', 'Agenda pendiente de publicación']
]) test(`agenda: ${instant} en Panamá`, () => {
assert.equal(countdownMessage(publicationDate, new Date(instant)), expected);
});

test('resta fechas calendario, incluso entre meses, sin redondear horas', () => {
assert.equal(countdownMessage('2028-03-01', new Date('2028-02-29T23:59:00-05:00')), 'Falta 1 día para publicar la agenda');
});

test('la fecha sigue visible sin JavaScript y no hay anuncios en vivo', () => {
assert.match(html, /datetime="2026-10-02">viernes 2 de octubre de 2026<\/time>/);
assert.match(html, /id="agenda-countdown"[^>]*aria-live="off">La hora de publicación está por confirmar\./);
});

test('actualiza al cambiar el día y al volver a la pestaña, sin reescribir el mismo texto', () => {
let instant = '2026-10-02T04:59:59Z';
let value = '';
let writes = 0;
let interval;
let onVisibility;
const counter = { get textContent() { return value; }, set textContent(next) { value = next; writes++; } };
const context = {
Intl,
Date: class extends Date { constructor() { super(instant); } },
document: {
hidden: false,
getElementById: id => id === 'agenda-countdown' ? counter : { getAttribute: () => publicationDate },
addEventListener: (event, callback) => { assert.equal(event, 'visibilitychange'); onVisibility = callback; }
},
window: { setInterval: (callback, delay) => { assert.equal(delay, 60000); interval = callback; } }
};
vm.runInNewContext(fs.readFileSync('2026/js/agenda-countdown.js', 'utf8'), context);
assert.equal(value, 'Falta 1 día para publicar la agenda');
interval();
assert.equal(writes, 1);
instant = '2026-10-02T05:00:00Z';
interval();
assert.equal(value, 'La agenda se publica hoy');
instant = '2026-10-03T05:00:00Z';
onVisibility();
assert.equal(value, 'Agenda pendiente de publicación');
});
Loading