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:
parent
60881ef202
commit
ef644001d2
1 changed files with 714 additions and 65 deletions
779
home/index.html
779
home/index.html
|
|
@ -80,6 +80,26 @@
|
|||
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 {
|
||||
background: #1a6bbf;
|
||||
|
|
@ -161,7 +181,7 @@
|
|||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Events ─────────────────────────────────────────────────── */
|
||||
/* ── Section titles ─────────────────────────────────────────── */
|
||||
.section-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
|
|
@ -179,6 +199,147 @@
|
|||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
|
|
@ -186,14 +347,97 @@
|
|||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* upcoming list inside agenda-layout uses flex column */
|
||||
#upcoming-list { display: flex; flex-direction: column; gap: .8rem; }
|
||||
|
||||
.event-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 1.2rem 1.4rem;
|
||||
box-shadow: 0 2px 10px rgba(26,107,191,.1);
|
||||
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 {
|
||||
font-size: .82rem;
|
||||
font-weight: 700;
|
||||
|
|
@ -205,6 +449,42 @@
|
|||
|
||||
.event-title { font-size: 1rem; font-weight: 600; }
|
||||
.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 {
|
||||
|
|
@ -268,6 +548,11 @@
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<div id="live-banner" hidden aria-live="polite" aria-atomic="true">
|
||||
<span class="live-dot" aria-hidden="true"></span>
|
||||
<strong>En ce moment :</strong> <span id="live-text"></span>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="logo-wrap">
|
||||
<img src="https://static.alpinux.org/logo/alpinux-logo.png"
|
||||
|
|
@ -297,17 +582,45 @@
|
|||
|
||||
<main>
|
||||
|
||||
<h2 class="section-title">Prochains événements</h2>
|
||||
<div class="events" id="events-container">
|
||||
<div class="event-card" id="static-reunion">
|
||||
<div class="event-date">1<sup>er</sup> & 3<sup>e</sup> jeudis</div>
|
||||
<div class="event-title">Réunion mensuelle</div>
|
||||
<div class="event-where">📍 Dynamo Chambéry — 18h00</div>
|
||||
<h2 class="section-title">Agenda</h2>
|
||||
|
||||
<div class="agenda-layout">
|
||||
|
||||
<div class="planning-wrap">
|
||||
<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 class="event-card" id="static-installparty">
|
||||
<div class="event-date">Voir le calendrier</div>
|
||||
<div class="event-title">Install Party & ateliers</div>
|
||||
<div class="event-where">📅 <a href="https://installparty.alpinux.org" style="color:#1a6bbf;">installparty.alpinux.org</a></div>
|
||||
|
||||
<div class="upcoming-col">
|
||||
<div id="today-section" hidden>
|
||||
<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">×</button>
|
||||
<div id="ev-modal-title" class="ev-modal-title"></div>
|
||||
<div id="ev-modal-body" class="ev-modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -351,84 +664,420 @@
|
|||
</p>
|
||||
</footer>
|
||||
|
||||
|
||||
<script>
|
||||
(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) {
|
||||
return ics.replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, '');
|
||||
}
|
||||
|
||||
function parseDate(line) {
|
||||
const ci = line.indexOf(':');
|
||||
const params = line.substring(0, ci).toUpperCase();
|
||||
const val = line.substring(ci + 1).trim();
|
||||
const y = +val.substring(0, 4), mo = +val.substring(4, 6) - 1, d = +val.substring(6, 8);
|
||||
if (params.includes('VALUE=DATE') || /^\d{8}$/.test(val))
|
||||
function parseICSDate(line) {
|
||||
var ci = line.indexOf(':');
|
||||
var left = line.slice(0, ci).toUpperCase();
|
||||
var val = line.slice(ci + 1).trim();
|
||||
var y = +val.slice(0,4), mo = +val.slice(4,6)-1, d = +val.slice(6,8);
|
||||
if (left.indexOf('VALUE=DATE') >= 0 || /^\d{8}$/.test(val))
|
||||
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);
|
||||
const dt = val.endsWith('Z')
|
||||
var h = +val.slice(9,11), mi = +val.slice(11,13), s = +val.slice(13,15);
|
||||
var dt = val.slice(-1) === 'Z'
|
||||
? new Date(Date.UTC(y, mo, d, h, mi, s))
|
||||
: new Date(y, mo, d, h, mi, s);
|
||||
return { date: dt, allDay: false };
|
||||
}
|
||||
|
||||
function parseICS(text) {
|
||||
const lines = unfold(text).split(/\r?\n/);
|
||||
const events = [], cap = /^(.+?)(;[^:]*)?:(.*)$/;
|
||||
let cur = null;
|
||||
for (const line of lines) {
|
||||
if (line === 'BEGIN:VEVENT') { cur = {}; continue; }
|
||||
if (line === 'END:VEVENT') { if (cur && cur.start) events.push(cur); cur = null; continue; }
|
||||
if (!cur) continue;
|
||||
const m = cap.exec(line);
|
||||
if (!m) continue;
|
||||
const key = m[1].toUpperCase();
|
||||
const val = m[3];
|
||||
if (key === 'SUMMARY') cur.title = val.replace(/\\,/g, ',').replace(/\\n/g, ' ');
|
||||
if (key === 'LOCATION') cur.location = val.replace(/\\,/g, ',');
|
||||
if (key === 'DTSTART') cur.start = parseDate(line);
|
||||
}
|
||||
var events = [], cur = null;
|
||||
unfold(text).split(/\r?\n/).forEach(function(line) {
|
||||
if (line === 'BEGIN:VEVENT') { cur = { exdates: [] }; return; }
|
||||
if (line === 'END:VEVENT') { if (cur && cur.start) events.push(cur); cur = null; return; }
|
||||
if (!cur || line.indexOf(':') < 0) return;
|
||||
var ci = line.indexOf(':');
|
||||
var key = line.slice(0, ci).split(';')[0].toUpperCase();
|
||||
var val = line.slice(ci + 1);
|
||||
if (key === 'SUMMARY') cur.title = val.replace(/\\,/g, ',').replace(/\\n/g, ' ').trim();
|
||||
if (key === 'LOCATION') cur.location = val.replace(/\\,/g, ',').trim();
|
||||
if (key === 'DESCRIPTION') cur.desc = val.replace(/\\,/g, ',').replace(/\\n/g, '\n').replace(/\\;/g, ';').trim();
|
||||
if (key === 'DTSTART') { try { var ps = parseICSDate(line); cur.start = ps.date; cur.allDay = ps.allDay; } catch(e) {} }
|
||||
if (key === 'DTEND') { try { cur.end = parseICSDate(line).date; } catch(e) {} }
|
||||
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;
|
||||
}
|
||||
|
||||
function fmtDate(d, allDay) {
|
||||
const opts = { weekday: 'long', day: 'numeric', month: 'long' };
|
||||
if (!allDay) opts.hour = '2-digit', opts.minute = '2-digit';
|
||||
return d.toLocaleDateString('fr-FR', opts);
|
||||
/* ── RRULE expansion ─────────────────────────────────────────── */
|
||||
|
||||
function localDateStr(d) {
|
||||
return d.getFullYear() + '-' +
|
||||
String(d.getMonth()+1).padStart(2,'0') + '-' +
|
||||
String(d.getDate()).padStart(2,'0');
|
||||
}
|
||||
|
||||
function makeCard(ev) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'event-card';
|
||||
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 parseRRuleUntil(s) {
|
||||
var v = s.replace('Z','');
|
||||
return new Date(+v.slice(0,4), +v.slice(4,6)-1, +v.slice(6,8));
|
||||
}
|
||||
|
||||
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,'&').replace(/</g,'<').replace(/>/g,'>') + '</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() {
|
||||
var allEvents = [];
|
||||
try {
|
||||
const r = await fetch(CAL_URL);
|
||||
if (!r.ok) return;
|
||||
const text = await r.text();
|
||||
if (!text.trim()) return;
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
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;
|
||||
var r = await fetch(CAL_URL);
|
||||
if (r.ok) {
|
||||
var text = await r.text();
|
||||
if (text.trim()) allEvents = parseICS(text);
|
||||
}
|
||||
} catch(_) {}
|
||||
|
||||
const container = document.getElementById('events-container');
|
||||
const anchor = document.getElementById('static-installparty');
|
||||
const staticReunion = document.getElementById('static-reunion');
|
||||
if (staticReunion) staticReunion.remove();
|
||||
upcoming.forEach(ev => container.insertBefore(makeCard(ev), anchor));
|
||||
} catch (_) { /* silently keep static tiles */ }
|
||||
var now = new Date();
|
||||
|
||||
var weekStart = new Date(now);
|
||||
weekStart.setDate(now.getDate() - ((now.getDay() + 6) % 7));
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue