Remettre la page d'accueil au niveau de ce qui est en ligne

Le fichier versionné datait du 2026-05-03 ; la page servie par www.alpinux.org a continué
d'évoluer sur le serveur jusqu'au 2026-05-07 sans jamais revenir ici — agenda en grille,
bandeau « en ce moment », fiche d'événement, rendez-vous du jour.

Ce commit reprend telle quelle la page en ligne, pour que le dépôt reparte de la vérité.
Rien n'est modifié au passage : les changements viennent au commit suivant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ae3qPEPRrNkEY5LwcTTKc1
This commit is contained in:
Cédrix 2026-09-20 00:22:11 +02:00
parent 60881ef202
commit ef644001d2

View file

@ -80,6 +80,26 @@
min-height: 100vh; min-height: 100vh;
} }
/* ── Live banner ────────────────────────────────────────────── */
#live-banner {
background: #e8f5e9;
border-bottom: 3px solid #43a047;
color: #1b5e20;
padding: .65rem 1.5rem;
text-align: center;
font-size: .92rem;
}
.live-dot {
display: inline-block;
width: 9px; height: 9px;
background: #43a047;
border-radius: 50%;
margin-right: .45rem;
vertical-align: middle;
animation: blink 1.4s ease-in-out infinite;
}
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: .15; } }
/* ── Header ────────────────────────────────────────────────── */ /* ── Header ────────────────────────────────────────────────── */
header { header {
background: #1a6bbf; background: #1a6bbf;
@ -161,7 +181,7 @@
padding: 3rem 1.5rem; padding: 3rem 1.5rem;
} }
/* ── Events ─────────────────────────────────────────────────── */ /* ── Section titles ─────────────────────────────────────────── */
.section-title { .section-title {
font-size: 1.15rem; font-size: 1.15rem;
font-weight: 700; font-weight: 700;
@ -179,6 +199,147 @@
background: #e8f1fb; background: #e8f1fb;
} }
.subsection-title {
font-size: 1rem;
font-weight: 600;
color: #0f4e8f;
margin-bottom: .8rem;
}
/* ── Agenda two-column layout ───────────────────────────────── */
.agenda-layout {
display: grid;
grid-template-columns: auto 1fr;
gap: 2rem;
align-items: start;
margin-bottom: 3rem;
}
@media (max-width: 700px) {
.agenda-layout { grid-template-columns: 1fr; }
}
.upcoming-col { min-width: 0; }
.events-col {
display: flex;
flex-direction: column;
gap: .8rem;
}
/* ── Planning grid ──────────────────────────────────────────── */
.planning-wrap {
background: #fff;
border-radius: 10px;
padding: 1.4rem;
box-shadow: 0 2px 10px rgba(26,107,191,.08);
margin-bottom: 2rem;
overflow-x: auto;
}
.planning-layout {
display: flex;
align-items: flex-start;
gap: 6px;
}
.day-labels {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 22px;
flex-shrink: 0;
}
.day-labels span {
height: 14px;
line-height: 14px;
font-size: .68rem;
color: #bbb;
text-align: right;
min-width: 10px;
}
.planning-right {
display: flex;
flex-direction: column;
gap: 3px;
}
.planning-months {
display: flex;
gap: 3px;
height: 18px;
align-items: flex-end;
}
.planning-months span {
width: 14px;
font-size: .68rem;
color: #bbb;
white-space: nowrap;
overflow: visible;
flex-shrink: 0;
}
#planning-grid {
display: grid;
grid-template-rows: repeat(7, 14px);
gap: 3px;
grid-auto-flow: column;
grid-auto-columns: 14px;
}
.day-cell {
width: 14px;
height: 14px;
border-radius: 2px;
background: #ebedf0;
}
.day-cell.past { opacity: .4; }
.day-cell.ev1 { background: #9fc5e8; cursor: pointer; }
.day-cell.ev2 { background: #1a6bbf; cursor: pointer; }
.day-cell.today { outline: 2px solid #f5a623; outline-offset: 1px; }
.day-cell[role=button]:hover { filter: brightness(1.14); }
.day-cell[role=button]:focus-visible { outline: 2px solid #f5a623; outline-offset: 1px; }
#planning-detail {
margin-top: 1rem;
padding: .75rem 1rem;
background: #f3f6fb;
border-radius: 6px;
border-left: 3px solid #1a6bbf;
font-size: .88rem;
}
#planning-detail ul { margin: .35rem 0 0 1.1rem; }
#planning-detail li { margin: .2rem 0; }
#planning-detail .ev-loc { color: #777; font-size: .83rem; }
.planning-legend {
margin-top: .9rem;
font-size: .75rem;
color: #bbb;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.lc {
display: inline-block;
width: 14px; height: 14px;
border-radius: 2px;
vertical-align: middle;
margin-right: .25rem;
}
.lc.ev1 { background: #9fc5e8; }
.lc.ev2 { background: #1a6bbf; }
.lc.today-swatch { background: #ebedf0; outline: 2px solid #f5a623; outline-offset: 1px; }
/* ── Event cards ────────────────────────────────────────────── */
.events { .events {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
@ -186,14 +347,97 @@
margin-bottom: 3rem; margin-bottom: 3rem;
} }
/* upcoming list inside agenda-layout uses flex column */
#upcoming-list { display: flex; flex-direction: column; gap: .8rem; }
.event-card { .event-card {
background: #fff; background: #fff;
border-radius: 10px; border-radius: 10px;
padding: 1.2rem 1.4rem; padding: 1.2rem 1.4rem;
box-shadow: 0 2px 10px rgba(26,107,191,.1); box-shadow: 0 2px 10px rgba(26,107,191,.1);
border-left: 4px solid #1a6bbf; border-left: 4px solid #1a6bbf;
position: relative;
} }
button.event-card {
text-align: left;
border: none;
width: 100%;
cursor: pointer;
font-family: inherit;
transition: transform .15s, box-shadow .15s;
}
button.event-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(26,107,191,.18);
}
a.event-card {
text-decoration: none;
color: inherit;
transition: transform .15s, box-shadow .15s;
display: block;
}
a.event-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(26,107,191,.18);
}
.event-card--more {
border-left-color: #888;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #555;
}
.event-card--more:hover { color: #1a6bbf; }
.event-icon { font-size: 1.8rem; margin-bottom: .4rem; }
/* ── Modal ──────────────────────────────────────────────────── */
.ev-modal {
position: fixed; inset: 0;
background: rgba(0,0,0,.45);
display: flex; align-items: center; justify-content: center;
z-index: 1000;
padding: 1rem;
}
.ev-modal[hidden] { display: none; }
.ev-modal-box {
background: #fff;
border-radius: 12px;
padding: 2rem;
max-width: 480px;
width: 100%;
position: relative;
box-shadow: 0 8px 32px rgba(0,0,0,.25);
}
.ev-modal-close {
position: absolute; top: .8rem; right: 1rem;
background: none; border: none; font-size: 1.6rem;
cursor: pointer; color: #999; line-height: 1;
}
.ev-modal-close:hover { color: #333; }
.ev-modal-title {
font-size: 1.15rem;
font-weight: 700;
color: #0f4e8f;
margin-bottom: .8rem;
padding-right: 1.5rem;
}
.ev-modal-body { font-size: .92rem; line-height: 1.6; color: #444; }
.event-date { .event-date {
font-size: .82rem; font-size: .82rem;
font-weight: 700; font-weight: 700;
@ -205,6 +449,42 @@
.event-title { font-size: 1rem; font-weight: 600; } .event-title { font-size: 1rem; font-weight: 600; }
.event-where { font-size: .88rem; color: #666; margin-top: .3rem; } .event-where { font-size: .88rem; color: #666; margin-top: .3rem; }
.ev-has-desc {
position: absolute;
top: .55rem; right: .6rem;
background: #1a6bbf;
color: #fff;
font-size: .72rem;
font-weight: 700;
font-style: normal;
line-height: 1;
padding: .2rem .4rem;
border-radius: 4px;
letter-spacing: .02em;
}
/* ── Today events ───────────────────────────────────────────── */
.event-card--today {
border-left-color: #f5a623;
background: #fffcf0;
}
.ev-today-badge {
display: inline-block;
background: #f5a623;
color: #fff;
font-size: .68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
padding: .15rem .5rem;
border-radius: 3px;
margin-bottom: .45rem;
}
.event-card--past {
opacity: .6;
}
/* ── Services ───────────────────────────────────────────────── */ /* ── Services ───────────────────────────────────────────────── */
.services { .services {
@ -268,6 +548,11 @@
</head> </head>
<body> <body>
<div id="live-banner" hidden aria-live="polite" aria-atomic="true">
<span class="live-dot" aria-hidden="true"></span>
<strong>En ce moment&nbsp;:</strong>&nbsp;<span id="live-text"></span>
</div>
<header> <header>
<div class="logo-wrap"> <div class="logo-wrap">
<img src="https://static.alpinux.org/logo/alpinux-logo.png" <img src="https://static.alpinux.org/logo/alpinux-logo.png"
@ -297,17 +582,45 @@
<main> <main>
<h2 class="section-title">Prochains événements</h2> <h2 class="section-title">Agenda</h2>
<div class="events" id="events-container">
<div class="event-card" id="static-reunion"> <div class="agenda-layout">
<div class="event-date">1<sup>er</sup> &amp; 3<sup>e</sup> jeudis</div>
<div class="event-title">Réunion mensuelle</div> <div class="planning-wrap">
<div class="event-where">📍 Dynamo Chambéry — 18h00</div> <div class="planning-layout">
<div class="day-labels" aria-hidden="true">
<span>L</span><span>M</span><span>M</span><span>J</span><span>V</span><span>S</span><span>D</span>
</div>
<div class="planning-right">
<div id="planning-months" class="planning-months" aria-hidden="true"></div>
<div id="planning-grid" class="planning-grid"
role="grid" aria-label="Planning des 16 prochaines semaines"></div>
</div>
</div>
<div id="planning-detail" hidden></div>
<p class="planning-legend" aria-hidden="true">
<span><span class="lc ev1"></span>1 événement</span>
<span><span class="lc ev2"></span>2+ événements</span>
<span><span class="lc today-swatch"></span>Aujourd'hui</span>
</p>
</div> </div>
<div class="event-card" id="static-installparty">
<div class="event-date">Voir le calendrier</div> <div class="upcoming-col">
<div class="event-title">Install Party &amp; ateliers</div> <div id="today-section" hidden>
<div class="event-where">📅 <a href="https://installparty.alpinux.org" style="color:#1a6bbf;">installparty.alpinux.org</a></div> <h3 class="subsection-title">Rendez-vous du jour</h3>
<div class="events-col" id="today-list" style="margin-bottom:1.4rem"></div>
</div>
<h3 class="subsection-title">Prochains rendez-vous</h3>
<div class="events-col" id="upcoming-list"></div>
</div>
</div>
<div id="ev-modal" class="ev-modal" hidden aria-modal="true" role="dialog" aria-labelledby="ev-modal-title">
<div class="ev-modal-box">
<button class="ev-modal-close" aria-label="Fermer">&times;</button>
<div id="ev-modal-title" class="ev-modal-title"></div>
<div id="ev-modal-body" class="ev-modal-body"></div>
</div> </div>
</div> </div>
@ -351,84 +664,420 @@
</p> </p>
</footer> </footer>
<script> <script>
(function () { (function () {
const CAL_URL = '/public-calendars/n5BWPYsxw7FCYozM'; 'use strict';
var CAL_URL = '/public-calendars/n5BWPYsxw7FCYozM';
var DISPLAY_WEEKS = 16;
var RRULE_DAY = { SU:0, MO:1, TU:2, WE:3, TH:4, FR:5, SA:6 };
/* ── ICS parsing ─────────────────────────────────────────────── */
function unfold(ics) { function unfold(ics) {
return ics.replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, ''); return ics.replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, '');
} }
function parseDate(line) { function parseICSDate(line) {
const ci = line.indexOf(':'); var ci = line.indexOf(':');
const params = line.substring(0, ci).toUpperCase(); var left = line.slice(0, ci).toUpperCase();
const val = line.substring(ci + 1).trim(); var val = line.slice(ci + 1).trim();
const y = +val.substring(0, 4), mo = +val.substring(4, 6) - 1, d = +val.substring(6, 8); var y = +val.slice(0,4), mo = +val.slice(4,6)-1, d = +val.slice(6,8);
if (params.includes('VALUE=DATE') || /^\d{8}$/.test(val)) if (left.indexOf('VALUE=DATE') >= 0 || /^\d{8}$/.test(val))
return { date: new Date(y, mo, d), allDay: true }; return { date: new Date(y, mo, d), allDay: true };
const h = +val.substring(9, 11), mi = +val.substring(11, 13), s = +val.substring(13, 15); var h = +val.slice(9,11), mi = +val.slice(11,13), s = +val.slice(13,15);
const dt = val.endsWith('Z') var dt = val.slice(-1) === 'Z'
? new Date(Date.UTC(y, mo, d, h, mi, s)) ? new Date(Date.UTC(y, mo, d, h, mi, s))
: new Date(y, mo, d, h, mi, s); : new Date(y, mo, d, h, mi, s);
return { date: dt, allDay: false }; return { date: dt, allDay: false };
} }
function parseICS(text) { function parseICS(text) {
const lines = unfold(text).split(/\r?\n/); var events = [], cur = null;
const events = [], cap = /^(.+?)(;[^:]*)?:(.*)$/; unfold(text).split(/\r?\n/).forEach(function(line) {
let cur = null; if (line === 'BEGIN:VEVENT') { cur = { exdates: [] }; return; }
for (const line of lines) { if (line === 'END:VEVENT') { if (cur && cur.start) events.push(cur); cur = null; return; }
if (line === 'BEGIN:VEVENT') { cur = {}; continue; } if (!cur || line.indexOf(':') < 0) return;
if (line === 'END:VEVENT') { if (cur && cur.start) events.push(cur); cur = null; continue; } var ci = line.indexOf(':');
if (!cur) continue; var key = line.slice(0, ci).split(';')[0].toUpperCase();
const m = cap.exec(line); var val = line.slice(ci + 1);
if (!m) continue; if (key === 'SUMMARY') cur.title = val.replace(/\\,/g, ',').replace(/\\n/g, ' ').trim();
const key = m[1].toUpperCase(); if (key === 'LOCATION') cur.location = val.replace(/\\,/g, ',').trim();
const val = m[3]; if (key === 'DESCRIPTION') cur.desc = val.replace(/\\,/g, ',').replace(/\\n/g, '\n').replace(/\\;/g, ';').trim();
if (key === 'SUMMARY') cur.title = val.replace(/\\,/g, ',').replace(/\\n/g, ' '); if (key === 'DTSTART') { try { var ps = parseICSDate(line); cur.start = ps.date; cur.allDay = ps.allDay; } catch(e) {} }
if (key === 'LOCATION') cur.location = val.replace(/\\,/g, ','); if (key === 'DTEND') { try { cur.end = parseICSDate(line).date; } catch(e) {} }
if (key === 'DTSTART') cur.start = parseDate(line); if (key === 'RRULE') {
} var r = {};
val.split(';').forEach(function(p) {
var eq = p.indexOf('=');
if (eq > 0) r[p.slice(0, eq)] = p.slice(eq + 1);
});
cur.rrule = r;
}
if (key === 'EXDATE') {
val.split(',').forEach(function(v) {
try { cur.exdates.push(parseICSDate(line.slice(0, ci+1) + v.trim()).date); } catch(e) {}
});
}
});
return events; return events;
} }
function fmtDate(d, allDay) { /* ── RRULE expansion ─────────────────────────────────────────── */
const opts = { weekday: 'long', day: 'numeric', month: 'long' };
if (!allDay) opts.hour = '2-digit', opts.minute = '2-digit'; function localDateStr(d) {
return d.toLocaleDateString('fr-FR', opts); return d.getFullYear() + '-' +
String(d.getMonth()+1).padStart(2,'0') + '-' +
String(d.getDate()).padStart(2,'0');
} }
function makeCard(ev) { function parseRRuleUntil(s) {
const card = document.createElement('div'); var v = s.replace('Z','');
card.className = 'event-card'; return new Date(+v.slice(0,4), +v.slice(4,6)-1, +v.slice(6,8));
card.innerHTML =
'<div class="event-date">' + fmtDate(ev.start.date, ev.start.allDay) + '</div>' +
'<div class="event-title">' + ev.title + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : '');
return card;
} }
function getNthWeekday(year, month, jsDay, n) {
var d;
if (n > 0) {
d = new Date(year, month, 1);
while (d.getDay() !== jsDay) d.setDate(d.getDate() + 1);
d.setDate(d.getDate() + (n-1) * 7);
return d.getMonth() === month ? d : null;
}
d = new Date(year, month+1, 0);
while (d.getDay() !== jsDay) d.setDate(d.getDate() - 1);
d.setDate(d.getDate() + (n+1) * 7);
return d.getMonth() === month ? d : null;
}
function expandEvent(ev, from, to) {
if (!ev.rrule) {
return (ev.start >= from && ev.start <= to)
? [{ title: ev.title, location: ev.location, desc: ev.desc, allDay: ev.allDay, start: ev.start, end: ev.end }]
: [];
}
var rule = ev.rrule;
var freq = rule.FREQ;
var ivl = +(rule.INTERVAL || 1);
var cnt = +(rule.COUNT || 9999);
var until = rule.UNTIL ? parseRRuleUntil(rule.UNTIL) : null;
var dur = ev.end ? (ev.end - ev.start) : 0;
var exSet = {};
(ev.exdates || []).forEach(function(d) { exSet[localDateStr(d)] = true; });
var results = [];
function emit(d) {
if (exSet[localDateStr(d)]) return;
if (d < from || d > to) return;
results.push({ title: ev.title, location: ev.location, desc: ev.desc, allDay: ev.allDay,
start: d, end: dur ? new Date(d.getTime() + dur) : null });
}
var cur, y, m, n, occ, pm, pos;
if (freq === 'MONTHLY') {
y = from.getFullYear(); m = from.getMonth() - ivl; n = 0;
while (n < cnt) {
m += ivl;
while (m > 11) { m -= 12; y++; }
if (new Date(y, m, 1) > to) break;
if (until && new Date(y, m, 1) > until) break;
occ = null;
if (rule.BYDAY) {
pm = /^(-?\d+)?([A-Z]{2})$/.exec(rule.BYDAY);
if (pm) {
pos = rule.BYSETPOS ? +rule.BYSETPOS : (pm[1] ? +pm[1] : 1);
occ = getNthWeekday(y, m, RRULE_DAY[pm[2]], pos);
}
} else {
occ = new Date(y, m, ev.start.getDate());
}
if (occ) {
occ.setHours(ev.start.getHours(), ev.start.getMinutes(), ev.start.getSeconds());
if (occ >= ev.start) emit(occ);
}
n++;
}
} else if (freq === 'WEEKLY') {
cur = new Date(ev.start); n = 0;
while (cur < from) cur.setDate(cur.getDate() + 7 * ivl);
cur.setDate(cur.getDate() - 7 * ivl);
while (n < cnt && cur <= to) {
if (until && cur > until) break;
emit(new Date(cur));
cur.setDate(cur.getDate() + 7 * ivl);
n++;
}
} else if (freq === 'YEARLY') {
cur = new Date(ev.start); n = 0;
while (cur.getFullYear() < from.getFullYear()) cur.setFullYear(cur.getFullYear() + ivl);
if (cur > from) cur.setFullYear(cur.getFullYear() - ivl);
while (n < cnt && cur <= to) {
if (until && cur > until) break;
emit(new Date(cur));
cur.setFullYear(cur.getFullYear() + ivl);
n++;
}
}
return results;
}
function expandAll(events, from, to) {
var out = [];
events.forEach(function(ev) {
expandEvent(ev, from, to).forEach(function(o) { out.push(o); });
});
return out;
}
/* ── Rendering ───────────────────────────────────────────────── */
function fmtDate(d, allDay) {
var s = d.toLocaleDateString('fr-FR', { weekday:'long', day:'numeric', month:'long' });
if (!allDay) s += ' à ' + d.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' });
return s;
}
function fmtTime(d) {
return d.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' });
}
function buildGrid(expanded) {
var now = new Date();
var todayKey = localDateStr(now);
var weekStart = new Date(now);
weekStart.setDate(now.getDate() - ((now.getDay() + 6) % 7));
weekStart.setHours(0, 0, 0, 0);
var evMap = {};
expanded.forEach(function(ev) {
var k = localDateStr(ev.start);
if (!evMap[k]) evMap[k] = [];
evMap[k].push(ev);
});
/* Month labels */
var monthsEl = document.getElementById('planning-months');
var lastMonth = -1;
monthsEl.innerHTML = '';
for (var w = 0; w < DISPLAY_WEEKS; w++) {
var md = new Date(weekStart);
md.setDate(weekStart.getDate() + w * 7);
var span = document.createElement('span');
if (md.getMonth() !== lastMonth) {
span.textContent = md.toLocaleDateString('fr-FR', { month: 'short' });
lastMonth = md.getMonth();
}
monthsEl.appendChild(span);
}
/* Grid cells */
var grid = document.getElementById('planning-grid');
var detail = document.getElementById('planning-detail');
grid.innerHTML = '';
for (var w = 0; w < DISPLAY_WEEKS; w++) {
for (var dd = 0; dd < 7; dd++) {
var cd = new Date(weekStart);
cd.setDate(weekStart.getDate() + w * 7 + dd);
var key = localDateStr(cd);
var dayEvs = evMap[key] || [];
var past = cd < now && key !== todayKey;
var cell = document.createElement('div');
cell.className = 'day-cell' +
(dayEvs.length === 1 ? ' ev1' : dayEvs.length >= 2 ? ' ev2' : '') +
(past ? ' past' : '') +
(key === todayKey ? ' today' : '');
if (dayEvs.length) {
cell.title = cd.toLocaleDateString('fr-FR', { weekday:'long', day:'numeric', month:'long' }) +
'\n' + dayEvs.map(function(e) { return '• ' + e.title; }).join('\n');
cell.setAttribute('role', 'button');
cell.setAttribute('tabindex', '0');
(function(dateCopy, evsCopy) {
cell.addEventListener('click', function() { showDetail(dateCopy, evsCopy, detail); });
cell.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') showDetail(dateCopy, evsCopy, detail);
});
})(new Date(cd), dayEvs.slice());
}
grid.appendChild(cell);
}
}
}
function showDetail(date, evs, el) {
el.hidden = false;
el.innerHTML = '<strong>' +
date.toLocaleDateString('fr-FR', { weekday:'long', day:'numeric', month:'long', year:'numeric' }) +
'</strong><ul>' +
evs.map(function(e) {
var time = e.allDay ? '' :
' <span class="ev-loc">' +
e.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) + '</span>';
return '<li>' + e.title + time +
(e.desc ? ' <span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
(e.location ? '<span class="ev-loc"> — ' + e.location + '</span>' : '') +
'</li>';
}).join('') + '</ul>';
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function openModal(ev) {
var modal = document.getElementById('ev-modal');
var title = document.getElementById('ev-modal-title');
var body = document.getElementById('ev-modal-body');
var opts = { weekday:'long', day:'numeric', month:'long', year:'numeric' };
var timeOpts = { hour:'2-digit', minute:'2-digit' };
var dateStr = ev.start.toLocaleDateString('fr-FR', opts);
if (!ev.allDay) {
dateStr += ' à ' + ev.start.toLocaleTimeString('fr-FR', timeOpts);
if (ev.end) dateStr += ' – ' + ev.end.toLocaleTimeString('fr-FR', timeOpts);
}
title.textContent = ev.title;
var descHtml = '';
if (ev.desc) {
descHtml = '<p style="margin-top:.8rem;color:#555;font-size:.88rem;white-space:pre-line">' +
ev.desc.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</p>';
}
body.innerHTML =
'<p>📅 ' + dateStr + '</p>' +
(ev.location ? '<p style="margin-top:.4rem">📍 ' + ev.location + '</p>' : '') +
descHtml;
modal.hidden = false;
document.getElementById('ev-modal').querySelector('.ev-modal-close').focus();
}
function buildTodayEvents(expanded) {
var now = new Date();
var todayKey = localDateStr(now);
var todayEvs = expanded
.filter(function(e) { return localDateStr(e.start) === todayKey; })
.sort(function(a, b) { return a.start - b.start; });
var section = document.getElementById('today-section');
if (!todayEvs.length) { section.hidden = true; return; }
section.hidden = false;
var container = document.getElementById('today-list');
container.innerHTML = '';
todayEvs.forEach(function(ev) {
var card = document.createElement('button');
card.className = 'event-card event-card--today';
var isPast = ev.end ? ev.end < now : (!ev.allDay && ev.start < now);
if (isPast) card.classList.add('event-card--past');
card.innerHTML =
'<span class="ev-today-badge">Aujourd\'hui</span>' +
(ev.desc ? '<span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
'<div class="event-date">' +
(ev.allDay ? 'Toute la journée'
: ev.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) +
(ev.end ? ' – ' + ev.end.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) : '')) +
'</div>' +
'<div class="event-title">' + ev.title + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : '');
card.addEventListener('click', function() { openModal(ev); });
container.appendChild(card);
});
}
function buildUpcoming(expanded) {
var now = new Date();
var todayKey = localDateStr(now);
var upcoming = expanded
.filter(function(e) { return e.start >= now && localDateStr(e.start) !== todayKey; })
.sort(function(a, b) { return a.start - b.start; })
.slice(0, 5);
var container = document.getElementById('upcoming-list');
container.innerHTML = '';
upcoming.forEach(function(ev) {
var card = document.createElement('button');
card.className = 'event-card';
card.innerHTML =
(ev.desc ? '<span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
'<div class="event-date">' + fmtDate(ev.start, ev.allDay) + '</div>' +
'<div class="event-title">' + ev.title + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : '');
card.addEventListener('click', function() { openModal(ev); });
container.appendChild(card);
});
var more = document.createElement('a');
more.className = 'event-card event-card--more';
more.href = 'https://wiki.alpinux.org/alpinux/evenements/';
more.innerHTML =
'<div class="event-icon">📅</div>' +
'<div class="event-title">Tous les événements</div>' +
'<div class="event-where">wiki.alpinux.org/alpinux/evenements</div>';
container.appendChild(more);
}
/* ── Modal close handlers ────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function() {
var modal = document.getElementById('ev-modal');
modal.querySelector('.ev-modal-close').addEventListener('click', function() {
modal.hidden = true;
});
modal.addEventListener('click', function(e) {
if (e.target === modal) modal.hidden = true;
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') modal.hidden = true;
});
});
function updateBanner(allEvents) {
var now = new Date();
var from = new Date(now.getTime() - 86400000);
var to = new Date(now.getTime() + 86400000);
var active = expandAll(allEvents, from, to).filter(function(e) {
return e.end && e.start <= now && e.end >= now;
});
var banner = document.getElementById('live-banner');
var liveText = document.getElementById('live-text');
if (active.length) {
liveText.textContent = active.map(function(e) {
return e.title +
(e.location ? ' — ' + e.location : '') +
(e.end ? ' (jusqu’à ' + fmtTime(e.end) + ')' : '');
}).join(' | ');
banner.hidden = false;
} else {
banner.hidden = true;
}
}
/* ── Main ────────────────────────────────────────────────────── */
async function load() { async function load() {
var allEvents = [];
try { try {
const r = await fetch(CAL_URL); var r = await fetch(CAL_URL);
if (!r.ok) return; if (r.ok) {
const text = await r.text(); var text = await r.text();
if (!text.trim()) return; if (text.trim()) allEvents = parseICS(text);
const now = new Date(); }
now.setHours(0, 0, 0, 0); } catch(_) {}
const upcoming = parseICS(text)
.filter(e => e.start.date >= now)
.sort((a, b) => a.start.date - b.start.date)
.slice(0, 6);
if (!upcoming.length) return;
const container = document.getElementById('events-container'); var now = new Date();
const anchor = document.getElementById('static-installparty');
const staticReunion = document.getElementById('static-reunion'); var weekStart = new Date(now);
if (staticReunion) staticReunion.remove(); weekStart.setDate(now.getDate() - ((now.getDay() + 6) % 7));
upcoming.forEach(ev => container.insertBefore(makeCard(ev), anchor)); weekStart.setHours(0, 0, 0, 0);
} catch (_) { /* silently keep static tiles */ }
var gridTo = new Date(weekStart);
gridTo.setDate(weekStart.getDate() + DISPLAY_WEEKS * 7);
var gridExpanded = expandAll(allEvents, weekStart, gridTo);
buildGrid(gridExpanded);
buildTodayEvents(gridExpanded);
buildUpcoming(gridExpanded);
updateBanner(allEvents);
setInterval(function() { updateBanner(allEvents); }, 60000);
} }
document.addEventListener('DOMContentLoaded', load); document.addEventListener('DOMContentLoaded', load);