alpinux-www/site/index.html
Cédrix ddf3f79e7a Rendre cliquables les adresses écrites dans l'agenda
« infos et inscriptions sur https://installparty.alpinux.org » s'affichait
en texte mort dans la fiche d'un événement : à recopier à la main. Les
adresses du descriptif et du lieu deviennent des liens, la ponctuation
qui termine la phrase restant en dehors.

Le texte vient du calendrier, donc de quelqu'un qui écrit du texte et
non du HTML : il est échappé avant d'être lié, et les titres et lieux
des cartes le sont aussi, ce qui n'était pas le cas jusqu'ici.

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

1429 lines
46 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; }
.ev-modal-body a { color: #1a6bbf; word-break: break-word; }
.ev-modal-body a:hover { text-decoration: none; }
.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; }
/* ── Nouveautés ─────────────────────────────────────────────── */
#news-section { margin-top: 3rem; }
.news {
list-style: none;
border-left: 2px solid #dce8f5;
margin-left: .5rem;
padding-left: 1.5rem;
}
.news li {
position: relative;
padding-bottom: 1.6rem;
}
.news li:last-child { padding-bottom: 0; }
.news li::before {
content: '';
position: absolute;
left: calc(-1.5rem - 6px);
top: .35rem;
width: 10px;
height: 10px;
border-radius: 50%;
background: #9fc5e8;
box-shadow: 0 0 0 3px #f3f6fb;
}
.news li:first-child::before { background: #1a6bbf; }
.news-when {
font-size: .78rem;
font-weight: 700;
color: #7a8ba0;
letter-spacing: .06em;
text-transform: uppercase;
display: block;
margin-bottom: .2rem;
}
.news-title {
font-size: 1rem;
font-weight: 600;
line-height: 1.35;
color: #1a1a2e;
text-decoration: none;
}
a.news-title:hover { color: #1a6bbf; text-decoration: underline; }
.news-text {
font-size: .92rem;
line-height: 1.6;
color: #4a5a6a;
margin-top: .25rem;
max-width: 62ch;
}
/* ── 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;
}
/* Un service annoncé mais pas encore ouvert : visible, pas cliquable. */
.service-card.a-venir {
background: #fbfcfe;
border: 1px dashed #c9d8e8;
box-shadow: none;
color: #8a99aa;
cursor: default;
}
.service-card.a-venir:hover {
transform: none;
box-shadow: none;
color: #8a99aa;
}
.service-card.a-venir .service-icon { filter: grayscale(1); opacity: .6; }
.service-bientot {
display: inline-block;
margin-top: .5rem;
font-size: .68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: #7a8ba0;
background: #eef3f9;
border-radius: 3px;
padding: .15rem .5rem;
}
.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="/modifications.html">Mises à jour du wiki</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>
&nbsp;·&nbsp;
<a href="/modifications.html">Toutes les mises à jour →</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>
<div class="service-card a-venir" aria-disabled="true">
<div class="service-icon">🎮</div>
<div class="service-name">Jeux &amp; quiz</div>
<div class="service-desc">Mini-jeux et questionnaires interactifs</div>
<span class="service-bientot">Bientôt</span>
</div>
</div>
<section id="news-section" hidden>
<h2 class="section-title">Nouveautés</h2>
<ol class="news" id="news-list"></ol>
</section>
</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);
}
}
}
/* Le calendrier est saisi par des humains : son texte est du texte, jamais du HTML. */
function echapper(texte) {
return String(texte == null ? '' : texte)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
/* Rend cliquables les adresses écrites dans un descriptif. Le texte reçu est déjà
échappé — le motif s'arrête donc avant une balise — et la ponctuation qui termine
une phrase reste en dehors du lien. */
function avecLiens(texteEchappe) {
return texteEchappe.replace(/https?:\/\/[^\s<]+/g, function(url) {
var queue = '';
var fin = /[.,;:!?»)\]]+$/.exec(url);
if (fin) {
queue = fin[0];
url = url.slice(0, url.length - queue.length);
}
return '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>' + queue;
});
}
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>' + echapper(e.title) + time +
(e.desc ? ' <span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
(e.location ? '<span class="ev-loc"> — ' + echapper(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">' +
avecLiens(echapper(ev.desc)) + '</p>';
}
body.innerHTML =
'<p>📅 ' + dateStr + '</p>' +
(ev.location ? '<p style="margin-top:.4rem">📍 ' + avecLiens(echapper(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">' + echapper(ev.title) + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + echapper(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">' + echapper(ev.title) + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + echapper(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>
<!-- ── Nouveautés : le journal des évolutions du site ───────────────── -->
<script>
(function () {
'use strict';
var URL_JSON = 'nouveautes.json';
var NB_MAX = 6; /* entrées affichées ; les plus anciennes restent dans le fichier */
function echappe(texte) {
var d = document.createElement('div');
d.textContent = texte;
return d.innerHTML;
}
/* '2026-09-23' lu comme une date locale : passer par le constructeur
décalerait l'affichage d'un jour selon le fuseau. */
function enDate(iso) {
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || ''));
return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
}
function quand(d) {
return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' });
}
function ligne(item, d) {
var li = document.createElement('li');
var titre = item.url
? '<a class="news-title" href="' + echappe(item.url) + '">' + echappe(item.titre) + '</a>'
: '<span class="news-title">' + echappe(item.titre) + '</span>';
li.innerHTML =
'<time class="news-when" datetime="' + echappe(item.date) + '">' + echappe(quand(d)) + '</time>' +
titre +
(item.texte ? '<p class="news-text">' + echappe(item.texte) + '</p>' : '');
return li;
}
async function charger() {
var items;
try {
var r = await fetch(URL_JSON, { cache: 'no-cache' });
if (!r.ok) return;
items = await r.json();
} catch (_) { return; } /* fichier absent ou illisible : la section reste masquée */
if (!Array.isArray(items)) return;
var valides = items
.map(function (item) {
var d = item && item.titre ? enDate(item.date) : null;
return d ? { item: item, d: d } : null;
})
.filter(Boolean)
.sort(function (a, b) { return b.d - a.d; });
if (!valides.length) return;
var liste = document.getElementById('news-list');
valides.slice(0, NB_MAX).forEach(function (v) { liste.appendChild(ligne(v.item, v.d)); });
document.getElementById('news-section').hidden = false;
}
document.addEventListener('DOMContentLoaded', charger);
})();
</script>
</body>
</html>