diff --git a/countdown.php b/countdown.php index ca8701d..685c826 100644 --- a/countdown.php +++ b/countdown.php @@ -20,7 +20,7 @@ setSecurityHeaders(); - + @@ -40,9 +40,7 @@ setSecurityHeaders(); @@ -54,15 +52,19 @@ setSecurityHeaders(); - + @@ -99,10 +101,8 @@ setSecurityHeaders();
-

La plateforme ouvrira ses portes le getTimestamp()); +

La plateforme ouvrira ses portes le (heure de La Réunion).

diff --git a/includes/structured-data.php b/includes/structured-data.php index a4f8b12..83421e4 100644 --- a/includes/structured-data.php +++ b/includes/structured-data.php @@ -430,22 +430,62 @@ function formatDateISO8601($dateString) { } /** - * Tronque un texte à une longueur donnée + * Formate une date en français (remplace strftime, déprécié depuis PHP 8.1) + * + * Utilise IntlDateFormatter quand l'extension intl est disponible, + * sinon replie sur un formatage manuel (mois français en toutes lettres). + * + * @param DateTimeInterface $date Date à formater + * @param bool $withTime true pour ajouter « à HH:mm » + * @return string Date formatée, ex. « 11 octobre 2025 » ou « 11 octobre 2025 à 00:00 » + */ +function formatDateFr($date, $withTime = false) { + if (class_exists('IntlDateFormatter')) { + $formatter = new IntlDateFormatter( + 'fr_FR', + IntlDateFormatter::FULL, + IntlDateFormatter::FULL, + $date->getTimezone(), // Le formateur ignore le fuseau du DateTime sans ça + IntlDateFormatter::GREGORIAN, + $withTime ? "d MMMM yyyy 'à' HH:mm" : 'd MMMM yyyy' + ); + $formatted = $formatter->format($date); + if ($formatted !== false) { + return $formatted; + } + } + + // Repli sans extension intl : mois français en toutes lettres + $months = [ + 1 => 'janvier', 'février', 'mars', 'avril', 'mai', 'juin', + 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre' + ]; + $formatted = $date->format('j') . ' ' . $months[(int) $date->format('n')] . ' ' . $date->format('Y'); + + if ($withTime) { + $formatted .= ' à ' . $date->format('H:i'); + } + + return $formatted; +} + +/** + * Tronque un texte à une longueur donnée (en caractères UTF-8) * * @param string $text Texte à tronquer * @param int $length Longueur maximale * @return string Texte tronqué */ function truncateText($text, $length = 200) { - if (strlen($text) <= $length) { + if (mb_strlen($text) <= $length) { return $text; } - $truncated = substr($text, 0, $length); - $lastSpace = strrpos($truncated, ' '); + $truncated = mb_substr($text, 0, $length); + $lastSpace = mb_strrpos($truncated, ' '); if ($lastSpace !== false) { - $truncated = substr($truncated, 0, $lastSpace); + $truncated = mb_substr($truncated, 0, $lastSpace); } return $truncated . '...'; diff --git a/js/countdown.js b/js/countdown.js index b2d8862..d7f98b8 100644 --- a/js/countdown.js +++ b/js/countdown.js @@ -2,9 +2,58 @@ * Script de compte à rebours pour la page de maintenance */ +/** + * Convertit une date ISO 8601 en timestamp, compatible Safari. + * + * Safari renvoie Invalid Date pour les formats non strictement ISO + * (ex. « 2025-10-11 00:00:00 » émis historiquement par PHP) : la chaîne + * est donc analysée manuellement. Sans fuseau explicite, la date est + * interprétée en UTC — PHP émet de toute façon toujours un décalage. + * + * @param {string|number} value Date ISO 8601 (ou timestamp déjà numérique) + * @returns {number} Timestamp en millisecondes + */ +function parseTargetDate(value) { + if (typeof value === 'number') { + return new Date(value).getTime(); + } + + const match = String(value).match( + /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/ + ); + + if (!match) { + // Repli sur l'analyse native pour les formats inattendus + return new Date(value).getTime(); + } + + const [, year, month, day, hours = '0', minutes = '0', seconds = '0', offset] = match; + + let timestamp = Date.UTC( + parseInt(year, 10), + parseInt(month, 10) - 1, + parseInt(day, 10), + parseInt(hours, 10), + parseInt(minutes, 10), + parseInt(seconds, 10) + ); + + if (offset && offset !== 'Z') { + // Un décalage « +02:00 » signifie « 2 h d'avance sur UTC » : on le soustrait + const sign = offset[0] === '+' ? 1 : -1; + const offsetDigits = offset.slice(1).replace(':', ''); + const offsetMinutes = sign * ( + parseInt(offsetDigits.slice(0, 2), 10) * 60 + parseInt(offsetDigits.slice(2, 4), 10) + ); + timestamp -= offsetMinutes * 60 * 1000; + } + + return timestamp; +} + class CountdownTimer { constructor(targetDate) { - this.targetDate = new Date(targetDate).getTime(); + this.targetDate = parseTargetDate(targetDate); this.elements = { days: document.getElementById('countdown-days'), hours: document.getElementById('countdown-hours'), diff --git a/tests/js/countdown-test.js b/tests/js/countdown-test.js index de53e85..5e4996c 100644 --- a/tests/js/countdown-test.js +++ b/tests/js/countdown-test.js @@ -80,11 +80,17 @@ function loadCountdown({ elements = null, now = FIXED_NOW } = {}) { }; vm.createContext(sandbox); - const CountdownTimer = vm.runInContext(COUNTDOWN_SOURCE + '\nCountdownTimer;', sandbox); + const { CountdownTimer, parseTargetDate } = vm.runInContext( + COUNTDOWN_SOURCE + '\n({ CountdownTimer, parseTargetDate });', + sandbox + ); - return { CountdownTimer, window: windowMock, timers }; + return { CountdownTimer, parseTargetDate, window: windowMock, timers }; } +// Convertit un timestamp en chaîne ISO 8601, comme le fait PHP via DateTime::ATOM +const toISO = (timestamp) => new Date(timestamp).toISOString(); + describe('CountdownTimer', () => { test('formatNumber complète les nombres sur deux chiffres', () => { const { CountdownTimer } = loadCountdown(); @@ -104,7 +110,7 @@ describe('CountdownTimer', () => { const elements = makeElements(); const { CountdownTimer } = loadCountdown({ elements }); - const timer = new CountdownTimer(FIXED_NOW + distance); + const timer = new CountdownTimer(toISO(FIXED_NOW + distance)); try { assert.strictEqual(elements['countdown-days'].textContent, '02'); @@ -121,7 +127,7 @@ describe('CountdownTimer', () => { const elements = makeElements(); const { CountdownTimer } = loadCountdown({ elements }); - const timer = new CountdownTimer(FIXED_NOW + distance); + const timer = new CountdownTimer(toISO(FIXED_NOW + distance)); try { assert.strictEqual(elements['countdown-days'].textContent, '01'); @@ -136,7 +142,7 @@ describe('CountdownTimer', () => { test('une date passée arrête le compte à rebours et redirige vers /', () => { const elements = makeElements(); const { CountdownTimer, window } = loadCountdown({ elements }); - const timer = new CountdownTimer(FIXED_NOW - 1000); + const timer = new CountdownTimer(toISO(FIXED_NOW - 1000)); try { // onComplete() redirige vers la page principale @@ -155,7 +161,7 @@ describe('CountdownTimer', () => { test('une distance nulle affiche zéro partout sans rediriger', () => { const elements = makeElements(); const { CountdownTimer, window } = loadCountdown({ elements }); - const timer = new CountdownTimer(FIXED_NOW); + const timer = new CountdownTimer(toISO(FIXED_NOW)); try { assert.strictEqual(elements['countdown-days'].textContent, '00'); @@ -170,7 +176,7 @@ describe('CountdownTimer', () => { test('fonctionne sans éléments DOM présents', () => { const { CountdownTimer } = loadCountdown({ elements: null }); - const timer = new CountdownTimer(FIXED_NOW + 60 * 1000); + const timer = new CountdownTimer(toISO(FIXED_NOW + 60 * 1000)); // Aucune erreur attendue malgré l'absence des éléments timer.stop(); @@ -178,7 +184,7 @@ describe('CountdownTimer', () => { test('démarre un intervalle d\'une seconde et stop() le nettoie', () => { const { CountdownTimer, timers } = loadCountdown(); - const timer = new CountdownTimer(FIXED_NOW + 60 * 1000); + const timer = new CountdownTimer(toISO(FIXED_NOW + 60 * 1000)); assert.strictEqual(timers.intervals.length, 1); assert.strictEqual(timers.intervals[0].delay, 1000); @@ -187,4 +193,78 @@ describe('CountdownTimer', () => { timer.stop(); assert.deepStrictEqual(timers.cleared, [timer.interval]); }); + + test('accepte une date ISO 8601 avec fuseau horaire (contrat PHP)', () => { + // FIXED_NOW = 2030-01-01T00:00:00Z + // Cible : 2030-01-02T05:00:00+04:00, soit 2030-01-02T01:00:00Z → 1 jour + 1 heure + const elements = makeElements(); + const { CountdownTimer } = loadCountdown({ elements }); + const timer = new CountdownTimer('2030-01-02T05:00:00+04:00'); + + try { + assert.strictEqual(elements['countdown-days'].textContent, '01'); + assert.strictEqual(elements['countdown-hours'].textContent, '01'); + assert.strictEqual(elements['countdown-minutes'].textContent, '00'); + assert.strictEqual(elements['countdown-seconds'].textContent, '00'); + } finally { + timer.stop(); + } + }); +}); + +describe('parseTargetDate', () => { + test('analyse une date ISO 8601 avec décalage horaire', () => { + const { parseTargetDate } = loadCountdown(); + + assert.strictEqual( + parseTargetDate('2025-10-11T00:00:00-04:00'), + Date.UTC(2025, 9, 11, 4, 0, 0) + ); + assert.strictEqual( + parseTargetDate('2025-10-11T00:00:00+02:00'), + Date.UTC(2025, 9, 10, 22, 0, 0) + ); + }); + + test('analyse une date ISO 8601 suffixée Z', () => { + const { parseTargetDate } = loadCountdown(); + + assert.strictEqual( + parseTargetDate('2025-10-11T00:00:00Z'), + Date.UTC(2025, 9, 11, 0, 0, 0) + ); + }); + + test('accepte le format historique avec espace (cas Safari)', () => { + const { parseTargetDate } = loadCountdown(); + + // Sans fuseau explicite, la date est interprétée en UTC + assert.strictEqual( + parseTargetDate('2025-10-11 00:00:00'), + Date.UTC(2025, 9, 11, 0, 0, 0) + ); + }); + + test('accepte les millisecondes et les décalages sans deux-points', () => { + const { parseTargetDate } = loadCountdown(); + + assert.strictEqual(parseTargetDate('2030-01-01T00:00:00.000Z'), FIXED_NOW); + assert.strictEqual( + parseTargetDate('2025-10-11T00:00:00-0400'), + Date.UTC(2025, 9, 11, 4, 0, 0) + ); + }); + + test('accepte un timestamp numérique tel quel', () => { + const { parseTargetDate } = loadCountdown(); + + assert.strictEqual(parseTargetDate(FIXED_NOW), FIXED_NOW); + }); + + test('retourne le même instant que new Date pour une date ISO valide', () => { + const { parseTargetDate } = loadCountdown(); + const iso = '2030-06-15T12:30:45+00:00'; + + assert.strictEqual(parseTargetDate(iso), new Date(iso).getTime()); + }); }); diff --git a/tests/php/format-test.php b/tests/php/format-test.php index 6187da7..06993c7 100644 --- a/tests/php/format-test.php +++ b/tests/php/format-test.php @@ -1,6 +1,6 @@