// Présentation des Event pour la liste : ordre, formatage date, défaut lieu. // Fonctions pures, sans DOM (testables). Locale et fuseau sont des constantes : // l'appli est mono-locale (bénévoles de Cugnaux). // config.js n'exporte rien : il pose globalThis.EchoConfig (cf. DECISIONS.md // 2026-07-17). L'import ne sert qu'à son effet de bord. import "./config.js"; import { lireStatut } from "./statut-mairie.js"; const LOCALE = "fr-FR"; const FUSEAU = "Europe/Paris"; // Source unique : le formulaire mairie a besoin de la même adresse (et de ses // coordonnées) que la liste. export const LIEU_PAR_DEFAUT = globalThis.EchoConfig.DEFAUTS.cafe.adresse; // Un événement horodaté porte un vrai instant : on l'affiche à l'heure de // Cugnaux, quel que soit le fuseau de la machine. const FORMAT_JOUR_PARIS = new Intl.DateTimeFormat(LOCALE, { timeZone: FUSEAU, dateStyle: "full", }); const FORMAT_HEURE_PARIS = new Intl.DateTimeFormat(LOCALE, { timeZone: FUSEAU, timeStyle: "short", }); // Sans timeZone, DÉLIBÉRÉMENT (ne pas « rétablir » FUSEAU ici) : une journée // entière est une date *flottante* (RFC 5545), sans fuseau. ical.js la // matérialise à minuit local ; la relire en local restitue le bon jour partout, // alors que la convertir vers Paris la ferait glisser au 9 depuis Tokyo. const FORMAT_JOUR_FLOTTANT = new Intl.DateTimeFormat(LOCALE, { dateStyle: "full", }); // Le parseur rend l'ordre du flux : l'ordre chronologique se décide ici. export function trierParDebut(evenements) { return [...evenements].sort((a, b) => a.debut.getTime() - b.debut.getTime()); } // Une journée entière n'a pas d'heure : afficher « 00:00 » serait un mensonge. export function formaterDate(evenement) { return evenement.journeeEntiere ? formaterJourneeEntiere(evenement) : formaterHorodate(evenement); } function formaterJourneeEntiere({ debut, fin }) { const premierJour = FORMAT_JOUR_FLOTTANT.format(debut); const dernierJour = veilleDe(fin); // Un seul jour, ou pas de DTEND exploitable (le parseur pose alors fin === debut). if (dernierJour.getTime() <= debut.getTime()) return premierJour; return `du ${premierJour} au ${FORMAT_JOUR_FLOTTANT.format(dernierJour)}`; } // DTEND est EXCLUSIF (RFC 5545) : un événement du 18 porte DTEND=19, un festival // du 18 au 20 porte DTEND=21. Sans ce recul d'un jour, on annoncerait un jour de // trop — une date FAUSSE, pas seulement laide. // Composantes locales (jamais « -24h ») : la date est flottante, et un // changement d'heure ferait basculer le jour. function veilleDe(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1); } function formaterHorodate({ debut, fin }) { const jourDebut = FORMAT_JOUR_PARIS.format(debut); const heureDebut = FORMAT_HEURE_PARIS.format(debut); // DTEND absent : le parseur pose fin === debut. « de 10:30 à 10:30 » serait absurde. if (fin.getTime() <= debut.getTime()) return `${jourDebut} à ${heureDebut}`; const jourFin = FORMAT_JOUR_PARIS.format(fin); const heureFin = FORMAT_HEURE_PARIS.format(fin); // Comparaison des jours tels qu'AFFICHÉS (donc à Paris) : un événement de // 22:00 à 02:00 change de jour, l'annoncer sur un seul serait un mensonge. if (jourFin === jourDebut) return `${jourDebut} de ${heureDebut} à ${heureFin}`; return `du ${jourDebut} à ${heureDebut} au ${jourFin} à ${heureFin}`; } // La plupart des événements se tiennent au café et n'ont pas de LOCATION : // afficher l'adresse par défaut plutôt qu'une ligne vide. export function formaterLieu(evenement) { return evenement.lieu.trim() || LIEU_PAR_DEFAUT; } // Statut mairie en clair. Le jour du tag est une date NUE (« AAAA-MM-JJ »), // jamais un instant : on la découpe, on ne la convertit pas — un Date la ferait // glisser d'un jour selon le fuseau de la machine. export function formaterStatut(statut) { if (statut.type === "ignoré") return "Ignoré"; if (statut.type !== "soumis") return ""; if (!statut.jour) return "Soumis"; const [, mois, jour] = statut.jour.split("-"); return `Soumis le ${jour}/${mois}`; } // Les ignorés restent VISIBLES (on doit pouvoir les dé-ignorer) mais cessent // d'encombrer : ils passent en fin de liste, chaque groupe gardant son ordre. export function repousserIgnores(evenements) { const ignore = (evenement) => lireStatut(evenement.categories).type === "ignoré"; return [...evenements.filter((e) => !ignore(e)), ...evenements.filter(ignore)]; }