alpinux-www/site/index.html
Cédrix ece449c26e Alléger le menu de l'accueil
Le wiki et ses mises à jour avaient chacun leur entrée alors que « Nos
services » les porte déjà, et que la section du wiki mène à la liste
complète. Sept entrées pour une page qui dit la même chose plus bas :
le menu répétait la page au lieu de l'ouvrir.

Restent les services qui n'ont pas leur propre section — portail,
Install Party, Gitea, listes — et l'adhésion, qui n'est pas un service.

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

1430 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>
<!-- Le wiki et ses mises à jour ne sont pas ici : « Nos services » les porte plus bas,
et la section du wiki mène à la liste complète. Un menu qui répète la page n'aide
personne. -->
<nav aria-label="Navigation principale">
<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>
&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>