Abonnement agenda (.ics) aux distributions

Un flux iCalendar généré depuis distributions.php, servi sur
/calendrier.ics, permet d'ajouter toutes les distributions à venir à son
agenda (iPhone, Android, Google Agenda, Outlook). En abonnement webcal://,
les agendas suivent les changements de dates sans rien réimporter.

Les heures sont converties depuis le champ `horaires` en tenant compte du
changement d'heure ; les distributions annulées sont exclues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre Martin
2026-09-23 00:54:39 +02:00
co-authored by Claude Opus 5.5
parent b705f25eff
commit b8f55e5609
4 changed files with 140 additions and 0 deletions
+2
View File
@@ -4,3 +4,5 @@ ErrorDocument 404 /404.html
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
RewriteRule ^calendrier\.ics$ calendrier.php [L]
+11
View File
@@ -69,3 +69,14 @@ Le script PHP :
- Les horaires propres à chaque date (la Guinguette finit plus tard)
Les dates sont triées chronologiquement et la première date future devient automatiquement "Prochaine distribution".
## Abonnement agenda (.ics)
`calendrier.php` (servi à l'adresse `/calendrier.ics` grâce au `.htaccess`)
génère un flux iCalendar à partir de `distributions.php`. Les personnes
abonnées depuis le bouton « Ajouter les distributions à mon agenda » voient
les modifications arriver automatiquement : il suffit de mettre à jour
`distributions.php`. Les distributions annulées ne figurent pas dans le flux.
Les horaires doivent rester au format `17h-19h` ou `17h-21h30` pour pouvoir
être convertis en heures de début et de fin.
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* Flux iCalendar (.ics) des distributions, généré depuis distributions.php.
*
* Utilisable en abonnement (webcal://) : les agendas se mettent à jour
* automatiquement quand le fichier de configuration change.
*/
require_once __DIR__ . '/distributions-utils.php';
/**
* Convertit une date et une heure au format '17h' ou '21h30' en horodatage UTC iCalendar
*/
function heureVersIcs(string $date, string $heure): string
{
if (!preg_match('/^(\d{1,2})h(\d{2})?$/', trim($heure), $m)) {
throw new InvalidArgumentException("Horaire invalide : $heure");
}
$dateHeure = new DateTime($date, new DateTimeZone('Europe/Paris'));
$dateHeure->setTime((int) $m[1], (int) ($m[2] ?? 0));
$dateHeure->setTimezone(new DateTimeZone('UTC'));
return $dateHeure->format('Ymd\THis\Z');
}
/**
* Échappe un texte pour une valeur iCalendar
*/
function echapperIcs(string $texte): string
{
return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $texte);
}
/**
* Replie une ligne à 75 octets maximum, sans couper un caractère multi-octets
*/
function plierLigneIcs(string $ligne): string
{
$resultat = '';
$courante = '';
foreach (mb_str_split($ligne) as $caractere) {
if (strlen($courante) + strlen($caractere) > 75) {
$resultat .= $courante . "\r\n";
$courante = ' ';
}
$courante .= $caractere;
}
return $resultat . $courante . "\r\n";
}
$lignes = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Les Frouzivores//Distributions//FR',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'X-WR-CALNAME:Distributions Les Frouzivores',
'X-WR-TIMEZONE:Europe/Paris',
'REFRESH-INTERVAL;VALUE=DURATION:P1D',
'X-PUBLISHED-TTL:P1D',
];
$maintenant = gmdate('Ymd\THis\Z');
foreach (chargerDistributions() as $distribution) {
if ($distribution['type'] === 'annulee') {
continue;
}
[$debut, $fin] = explode('-', $distribution['horaires']);
$lieu = getLieuInfo($distribution['lieu']);
$titre = 'Distribution Les Frouzivores';
if (!empty($distribution['label'])) {
$titre .= ' ' . $distribution['label'];
}
$lignes[] = 'BEGIN:VEVENT';
$lignes[] = 'UID:distribution-' . $distribution['date'] . '@lesfrouzivores.fr';
$lignes[] = 'DTSTAMP:' . $maintenant;
$lignes[] = 'DTSTART:' . heureVersIcs($distribution['date'], $debut);
$lignes[] = 'DTEND:' . heureVersIcs($distribution['date'], $fin);
$lignes[] = 'SUMMARY:' . echapperIcs($titre);
if ($lieu) {
$lignes[] = 'LOCATION:' . echapperIcs($lieu['nom'] . ', ' . $lieu['adresse'] . ', 31270 Frouzins');
}
$lignes[] = 'DESCRIPTION:' . echapperIcs(
"Distribution mensuelle des commandes passées sur local.direct.\n"
. 'Commander : https://www.local.direct/les-frouzivores'
);
$lignes[] = 'URL:https://lesfrouzivores.fr/';
$lignes[] = 'END:VEVENT';
}
$lignes[] = 'END:VCALENDAR';
header('Content-Type: text/calendar; charset=utf-8');
header('Content-Disposition: inline; filename="frouzivores-distributions.ics"');
foreach ($lignes as $ligne) {
echo plierLigneIcs($ligne);
}
+26
View File
@@ -324,6 +324,32 @@ $prochaineDistribution = trouverProchaineDistribution($distributions);
</div>
<?php endforeach; ?>
</div>
<div class="mt-8 text-center">
<a
href="webcal://lesfrouzivores.fr/calendrier.ics"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-green-600 hover:bg-green-700"
>
<i class="fas fa-calendar-plus mr-2"></i>
Ajouter les distributions à mon agenda
</a>
<p class="mt-3 text-sm text-gray-500">
Ou via
<a
href="https://calendar.google.com/calendar/render?cid=webcal://lesfrouzivores.fr/calendrier.ics"
class="text-green-700 underline hover:text-green-800"
>Google Agenda</a
>
· <a
href="/calendrier.ics"
download="frouzivores-distributions.ics"
class="text-green-700 underline hover:text-green-800"
>télécharger le fichier .ics</a
>
</p>
<p class="mt-1 text-xs text-gray-400">
Lagenda se met à jour tout seul si une date change.
</p>
</div>
</div>
</div>