alpinux-www/site/index.html
Cédrix f0a0c96e75 Renvoyer vers messagerie.alpinux.org depuis l'accueil
Les listes de diffusion existaient sans qu'on puisse les atteindre depuis
la page d'accueil. Trois portes, selon ce que vient chercher le visiteur :
la navigation et une carte dans « Nos services » mènent à l'inscription,
le pied de page mène directement aux archives, pour qui veut d'abord lire
ce qui a déjà été envoyé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYfYUZRcJmmEWJwAxZb4o2
2026-09-23 12:25:54 +02:00

1237 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Alpinux — Le LUG de Savoie. Groupe d'utilisateurs de Linux et logiciels libres à Chambéry. Réunions tous les 1er et 3e jeudis du mois.">
<meta name="robots" content="index, follow">
<title>Alpinux — Le LUG de Savoie</title>
<!-- Canonical -->
<link rel="canonical" href="https://alpinux.org/">
<!-- Favicons -->
<link rel="icon" type="image/x-icon" href="https://static.alpinux.org/logo/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="https://static.alpinux.org/logo/favicon-32.png">
<link rel="icon" type="image/png" sizes="96x96" href="https://static.alpinux.org/logo/favicon-96.png">
<link rel="apple-touch-icon" sizes="192x192" href="https://static.alpinux.org/logo/favicon-192.png">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="Alpinux">
<meta property="og:title" content="Alpinux — Le LUG de Savoie">
<meta property="og:description" content="Association dédiée à Linux et aux logiciels libres en Savoie. Réunions à la Dynamo Chambéry tous les 1er et 3e jeudis du mois.">
<meta property="og:url" content="https://alpinux.org/">
<meta property="og:image" content="https://static.alpinux.org/logo/alpinux-logo-512.png">
<meta property="og:locale" content="fr_FR">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Alpinux — Le LUG de Savoie">
<meta name="twitter:description" content="Association dédiée à Linux et aux logiciels libres en Savoie.">
<meta name="twitter:image" content="https://static.alpinux.org/logo/alpinux-logo-512.png">
<meta name="twitter:site" content="@alpinux">
<!-- Structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Alpinux",
"alternateName": "Alpinux — LUG de Savoie",
"url": "https://alpinux.org",
"logo": "https://static.alpinux.org/logo/alpinux-logo-512.png",
"description": "Association loi 1901 dédiée à Linux et aux logiciels libres en Savoie. Groupe d'utilisateurs Linux (LUG) basé à Chambéry.",
"foundingLocation": {
"@type": "Place",
"name": "Chambéry",
"address": {
"@type": "PostalAddress",
"addressLocality": "Chambéry",
"addressRegion": "Savoie",
"addressCountry": "FR"
}
},
"location": {
"@type": "Place",
"name": "Dynamo Chambéry",
"address": {
"@type": "PostalAddress",
"addressLocality": "Chambéry",
"addressRegion": "Savoie",
"addressCountry": "FR"
}
},
"sameAs": [
"https://mamot.fr/@alpinux",
"https://www.helloasso.com/associations/alpinux-le-lug-de-savoie",
"https://gitea.alpinux.org/alpinux.cedrica5l"
]
}
</script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #f3f6fb;
color: #1a1a2e;
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;
color: #fff;
padding: 3rem 1.5rem 2.5rem;
text-align: center;
}
.logo-wrap {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
flex-wrap: wrap;
}
.logo-img {
width: 90px;
height: 90px;
border-radius: 12px;
}
.logo-text {
text-align: left;
}
.brand {
font-size: 2.8rem;
line-height: 1;
color: #fff;
letter-spacing: -.02em;
}
.brand .light { font-weight: 300; }
.brand .bold { font-weight: 800; }
.tagline {
font-size: 1rem;
font-weight: 300;
letter-spacing: .18em;
color: rgba(255,255,255,.8);
margin-top: .3rem;
text-transform: lowercase;
}
.hero-text {
max-width: 640px;
margin: 1.8rem auto 0;
font-size: 1.05rem;
color: rgba(255,255,255,.88);
line-height: 1.7;
}
/* ── Nav links ──────────────────────────────────────────────── */
nav {
background: #0f4e8f;
display: flex;
justify-content: center;
gap: .2rem;
flex-wrap: wrap;
padding: .4rem 1rem;
}
nav a {
color: rgba(255,255,255,.85);
text-decoration: none;
font-size: .88rem;
padding: .45rem .9rem;
border-radius: 4px;
transition: background .15s;
}
nav a:hover { background: rgba(255,255,255,.15); color: #fff; }
/* ── Main ───────────────────────────────────────────────────── */
main {
max-width: 960px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
/* ── Section titles ─────────────────────────────────────────── */
.section-title {
font-size: 1.15rem;
font-weight: 700;
color: #0f4e8f;
margin-bottom: 1.2rem;
display: flex;
align-items: center;
gap: .6rem;
}
.section-title::after {
content: '';
flex: 1;
height: 2px;
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));
gap: 1rem;
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;
color: #1a6bbf;
letter-spacing: .06em;
text-transform: uppercase;
margin-bottom: .3rem;
}
.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;
}
/* ── Wiki : dernières mises à jour ──────────────────────────── */
#wiki-section { margin-bottom: 3rem; }
.wiki-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
}
.wiki-card {
background: #fff;
border-radius: 10px;
padding: 1.1rem 1.3rem;
box-shadow: 0 2px 10px rgba(26,107,191,.08);
border-left: 4px solid #9fc5e8;
text-decoration: none;
color: inherit;
display: block;
position: relative;
transition: transform .15s, box-shadow .15s;
}
.wiki-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(26,107,191,.18);
}
.wiki-card:hover .wiki-title { color: #1a6bbf; }
.wiki-when {
font-size: .78rem;
font-weight: 700;
color: #7a8ba0;
letter-spacing: .06em;
text-transform: uppercase;
margin-bottom: .3rem;
}
.wiki-title {
font-size: 1rem;
font-weight: 600;
line-height: 1.35;
padding-right: 4.2rem;
}
.wiki-badge {
position: absolute;
top: .7rem; right: .7rem;
background: #f5a623;
color: #fff;
font-size: .68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
padding: .15rem .5rem;
border-radius: 3px;
}
.wiki-more {
margin-top: 1rem;
font-size: .88rem;
}
.wiki-more a { color: #1a6bbf; text-decoration: none; }
.wiki-more a:hover { text-decoration: underline; }
/* ── Services ───────────────────────────────────────────────── */
.services {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.service-card {
background: #fff;
border-radius: 10px;
padding: 1.4rem;
box-shadow: 0 2px 10px rgba(26,107,191,.08);
text-align: center;
text-decoration: none;
color: #1a1a2e;
transition: transform .15s, box-shadow .15s;
display: block;
}
.service-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(26,107,191,.18);
color: #1a6bbf;
}
.service-icon {
font-size: 2rem;
margin-bottom: .6rem;
}
.service-name {
font-size: 1rem;
font-weight: 700;
margin-bottom: .3rem;
}
.service-desc {
font-size: .84rem;
color: #666;
line-height: 1.4;
}
/* ── Footer ─────────────────────────────────────────────────── */
footer {
background: #0f4e8f;
color: rgba(255,255,255,.7);
text-align: center;
padding: 1.5rem;
font-size: .85rem;
}
footer a { color: rgba(255,255,255,.85); text-decoration: none; }
footer a:hover { color: #fff; }
@media (max-width: 480px) {
.brand { font-size: 2.2rem; }
.logo-img { width: 70px; height: 70px; }
}
</style>
</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&nbsp;:</strong>&nbsp;<span id="live-text"></span>
</div>
<header>
<div class="logo-wrap">
<img src="https://static.alpinux.org/logo/alpinux-logo.png"
alt="Logo Alpinux — LUG de Savoie"
class="logo-img"
width="90" height="90">
<div class="logo-text">
<div class="brand">
<span class="light">A</span><span class="bold">l</span><span class="light">p</span><span class="bold">inux</span>
</div>
<div class="tagline">le LUG de Savoie</div>
</div>
</div>
<p class="hero-text">
Association loi 1901 dédiée à <strong>Linux et aux logiciels libres</strong> en Savoie.
Réunions tous les 1<sup>er</sup> et 3<sup>e</sup> jeudis du mois à la <strong>Dynamo Chambéry</strong>.
</p>
</header>
<nav aria-label="Navigation principale">
<a href="https://wiki.alpinux.org">Wiki</a>
<a href="https://portail.alpinux.org">Portail membres</a>
<a href="https://installparty.alpinux.org">Install Party</a>
<a href="https://gitea.alpinux.org">Gitea</a>
<a href="https://messagerie.alpinux.org">Listes de diffusion</a>
<a href="https://www.helloasso.com/associations/alpinux-le-lug-de-savoie">Adhérer</a>
</nav>
<main>
<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="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">&times;</button>
<div id="ev-modal-title" class="ev-modal-title"></div>
<div id="ev-modal-body" class="ev-modal-body"></div>
</div>
</div>
<section id="wiki-section" hidden>
<h2 class="section-title">Le wiki, fraîchement mis à jour</h2>
<div class="wiki-list" id="wiki-list"></div>
<p class="wiki-more">
<a href="https://wiki.alpinux.org">Parcourir tout le wiki →</a>
</p>
</section>
<h2 class="section-title">Nos services</h2>
<div class="services">
<a href="https://wiki.alpinux.org" class="service-card">
<div class="service-icon">📖</div>
<div class="service-name">Wiki</div>
<div class="service-desc">Guides, tutoriels et présentations</div>
</a>
<a href="https://portail.alpinux.org" class="service-card">
<div class="service-icon">🔐</div>
<div class="service-name">Portail membres</div>
<div class="service-desc">Espace adhérent, Nextcloud, Dolibarr</div>
</a>
<a href="https://installparty.alpinux.org" class="service-card">
<div class="service-icon">🐧</div>
<div class="service-name">Install Party</div>
<div class="service-desc">Événements &amp; inscriptions</div>
</a>
<a href="https://gitea.alpinux.org" class="service-card">
<div class="service-icon">💻</div>
<div class="service-name">Gitea</div>
<div class="service-desc">Dépôts de code de l'association</div>
</a>
<a href="https://messagerie.alpinux.org" class="service-card">
<div class="service-icon">✉️</div>
<div class="service-name">Listes de diffusion</div>
<div class="service-desc">S'abonner aux annonces, relire les archives</div>
</a>
<a href="https://dynamic.alpinux.org" class="service-card">
<div class="service-icon">🎮</div>
<div class="service-name">Jeux &amp; quiz</div>
<div class="service-desc">Mini-jeux et questionnaires interactifs</div>
</a>
</div>
</main>
<footer>
<p>© Alpinux — Association loi 1901 &nbsp;·&nbsp; Chambéry, Savoie</p>
<p style="margin-top:.4rem;">
<a href="https://wiki.alpinux.org/alpinux/">À propos</a> &nbsp;·&nbsp;
<a href="https://wiki.alpinux.org/alpinux/faq/">FAQ</a> &nbsp;·&nbsp;
<a href="https://wiki.alpinux.org/alpinux/donnees-personnelles/">Vos données</a> &nbsp;·&nbsp;
<a href="https://messagerie.alpinux.org/archives">Archives des listes</a> &nbsp;·&nbsp;
<a href="https://www.helloasso.com/associations/alpinux-le-lug-de-savoie">Adhérer</a>
</p>
</footer>
<script>
(function () {
'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 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 };
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) {
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;
}
/* ── RRULE expansion ─────────────────────────────────────────── */
function localDateStr(d) {
return d.getFullYear() + '-' +
String(d.getMonth()+1).padStart(2,'0') + '-' +
String(d.getDate()).padStart(2,'0');
}
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,'&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() {
var allEvents = [];
try {
var r = await fetch(CAL_URL);
if (r.ok) {
var text = await r.text();
if (text.trim()) allEvents = parseICS(text);
}
} catch(_) {}
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);
})();
</script>
<!-- ── Wiki : dernières pages créées ou modifiées ───────────────────── -->
<script>
(function () {
var URL_JSON = 'https://wiki.alpinux.org/derniers-articles.json';
var NB_MAX = 6; /* articles affichés sur l'accueil */
/* Le wiki date ses pages avec l'horodatage du commit qui a touché leur contenu :
un article déplacé ou renommé n'apparaît donc pas ici. */
function echappe(texte) {
var d = document.createElement('div');
d.textContent = texte;
return d.innerHTML;
}
function quand(iso) {
var d = new Date(iso);
if (isNaN(d)) return '';
var jour = new Date(d.getFullYear(), d.getMonth(), d.getDate());
var auj = new Date();
auj.setHours(0, 0, 0, 0);
var jours = Math.round((auj - jour) / 86400000);
if (jours <= 0) return "aujourd'hui";
if (jours === 1) return 'hier';
if (jours < 7) return 'il y a ' + jours + ' jours';
if (jours < 14) return 'la semaine dernière';
var opts = { day: 'numeric', month: 'long' };
if (d.getFullYear() !== auj.getFullYear()) opts.year = 'numeric';
return 'le ' + d.toLocaleDateString('fr-FR', opts);
}
function carte(article) {
var a = document.createElement('a');
a.className = 'wiki-card';
a.href = article.url;
a.innerHTML =
(article.nouveau ? '<span class="wiki-badge">Nouveau</span>' : '') +
'<div class="wiki-when">' +
(article.nouveau ? 'publié ' : 'mis à jour ') + echappe(quand(article.date)) +
'</div>' +
'<div class="wiki-title">' + echappe(article.titre) + '</div>';
return a;
}
async function charger() {
var articles;
try {
var r = await fetch(URL_JSON, { cache: 'no-cache' });
if (!r.ok) return;
articles = (await r.json()).articles;
} catch (_) { return; } /* wiki injoignable : la section reste masquée */
if (!Array.isArray(articles) || !articles.length) return;
var liste = document.getElementById('wiki-list');
articles.slice(0, NB_MAX).forEach(function (article) {
if (article && article.url && article.titre) liste.appendChild(carte(article));
});
if (liste.children.length) document.getElementById('wiki-section').hidden = false;
}
document.addEventListener('DOMContentLoaded', charger);
})();
</script>
</body>
</html>