L'accueil n'annonçait que six pages du wiki, et rien ne menait au reste : qui voulait savoir ce qui avait bougé depuis sa dernière visite n'avait nulle part où aller. modifications.html liste tout ce que le wiki publie, du plus récemment retouché au plus ancien, avec un filtre par titre ou par rubrique. Elle lit toutes-les-pages.json, que le build du wiki écrit à côté de derniers-articles.json ; tant que ce fichier n'existe pas, elle se rabat sur celui de l'accueil et le dit plutôt que de rester vide. 404.html reprend la charte et propose l'accueil, le wiki, les mises à jour et les listes. Elle attend encore sa directive Apache : le site répond aujourd'hui 200 et l'accueil à toute adresse inconnue, ce qui trompe le visiteur comme le moteur de recherche. Le README dit quoi poser dans le .htaccess du serveur. Les deux pages partagent commun.css plutôt que de recopier le style de l'accueil, qui garde le sien en ligne — une page, une requête. Et le garde-fou du déploiement, qui ne regardait que l'accueil, refuse désormais de mettre en ligne n'importe quelle page tronquée : une 404 servie à moitié serait aussi laide qu'une erreur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYfYUZRcJmmEWJwAxZb4o2
1372 lines
44 KiB
HTML
1372 lines
44 KiB
HTML
<!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; }
|
||
|
||
/* ── 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;
|
||
}
|
||
|
||
.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 :</strong> <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">×</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>
|
||
·
|
||
<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 & 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 & quiz</div>
|
||
<div class="service-desc">Mini-jeux et questionnaires interactifs</div>
|
||
</a>
|
||
</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 · Chambéry, Savoie</p>
|
||
<p style="margin-top:.4rem;">
|
||
<a href="https://wiki.alpinux.org/alpinux/">À propos</a> ·
|
||
<a href="https://wiki.alpinux.org/alpinux/faq/">FAQ</a> ·
|
||
<a href="https://wiki.alpinux.org/alpinux/donnees-personnelles/">Vos données</a> ·
|
||
<a href="https://messagerie.alpinux.org/archives">Archives des listes</a> ·
|
||
<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,'&').replace(/</g,'<').replace(/>/g,'>') + '</p>';
|
||
}
|
||
body.innerHTML =
|
||
'<p>📅 ' + dateStr + '</p>' +
|
||
(ev.location ? '<p style="margin-top:.4rem">📍 ' + ev.location + '</p>' : '') +
|
||
descHtml;
|
||
modal.hidden = false;
|
||
document.getElementById('ev-modal').querySelector('.ev-modal-close').focus();
|
||
}
|
||
|
||
function buildTodayEvents(expanded) {
|
||
var now = new Date();
|
||
var todayKey = localDateStr(now);
|
||
var todayEvs = expanded
|
||
.filter(function(e) { return localDateStr(e.start) === todayKey; })
|
||
.sort(function(a, b) { return a.start - b.start; });
|
||
|
||
var section = document.getElementById('today-section');
|
||
if (!todayEvs.length) { section.hidden = true; return; }
|
||
|
||
section.hidden = false;
|
||
var container = document.getElementById('today-list');
|
||
container.innerHTML = '';
|
||
todayEvs.forEach(function(ev) {
|
||
var card = document.createElement('button');
|
||
card.className = 'event-card event-card--today';
|
||
var isPast = ev.end ? ev.end < now : (!ev.allDay && ev.start < now);
|
||
if (isPast) card.classList.add('event-card--past');
|
||
card.innerHTML =
|
||
'<span class="ev-today-badge">Aujourd\'hui</span>' +
|
||
(ev.desc ? '<span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
|
||
'<div class="event-date">' +
|
||
(ev.allDay ? 'Toute la journée'
|
||
: ev.start.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) +
|
||
(ev.end ? ' – ' + ev.end.toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' }) : '')) +
|
||
'</div>' +
|
||
'<div class="event-title">' + ev.title + '</div>' +
|
||
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : '');
|
||
card.addEventListener('click', function() { openModal(ev); });
|
||
container.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function buildUpcoming(expanded) {
|
||
var now = new Date();
|
||
var todayKey = localDateStr(now);
|
||
var upcoming = expanded
|
||
.filter(function(e) { return e.start >= now && localDateStr(e.start) !== todayKey; })
|
||
.sort(function(a, b) { return a.start - b.start; })
|
||
.slice(0, 5);
|
||
|
||
var container = document.getElementById('upcoming-list');
|
||
container.innerHTML = '';
|
||
upcoming.forEach(function(ev) {
|
||
var card = document.createElement('button');
|
||
card.className = 'event-card';
|
||
card.innerHTML =
|
||
(ev.desc ? '<span class="ev-has-desc" title="Descriptif disponible">ℹ</span>' : '') +
|
||
'<div class="event-date">' + fmtDate(ev.start, ev.allDay) + '</div>' +
|
||
'<div class="event-title">' + ev.title + '</div>' +
|
||
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : '');
|
||
card.addEventListener('click', function() { openModal(ev); });
|
||
container.appendChild(card);
|
||
});
|
||
|
||
var more = document.createElement('a');
|
||
more.className = 'event-card event-card--more';
|
||
more.href = 'https://wiki.alpinux.org/alpinux/evenements/';
|
||
more.innerHTML =
|
||
'<div class="event-icon">📅</div>' +
|
||
'<div class="event-title">Tous les événements</div>' +
|
||
'<div class="event-where">wiki.alpinux.org/alpinux/evenements</div>';
|
||
container.appendChild(more);
|
||
}
|
||
|
||
/* ── Modal close handlers ────────────────────────────────────── */
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var modal = document.getElementById('ev-modal');
|
||
modal.querySelector('.ev-modal-close').addEventListener('click', function() {
|
||
modal.hidden = true;
|
||
});
|
||
modal.addEventListener('click', function(e) {
|
||
if (e.target === modal) modal.hidden = true;
|
||
});
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') modal.hidden = true;
|
||
});
|
||
});
|
||
|
||
function updateBanner(allEvents) {
|
||
var now = new Date();
|
||
var from = new Date(now.getTime() - 86400000);
|
||
var to = new Date(now.getTime() + 86400000);
|
||
var active = expandAll(allEvents, from, to).filter(function(e) {
|
||
return e.end && e.start <= now && e.end >= now;
|
||
});
|
||
var banner = document.getElementById('live-banner');
|
||
var liveText = document.getElementById('live-text');
|
||
if (active.length) {
|
||
liveText.textContent = active.map(function(e) {
|
||
return e.title +
|
||
(e.location ? ' — ' + e.location : '') +
|
||
(e.end ? ' (jusqu’à ' + fmtTime(e.end) + ')' : '');
|
||
}).join(' | ');
|
||
banner.hidden = false;
|
||
} else {
|
||
banner.hidden = true;
|
||
}
|
||
}
|
||
|
||
/* ── Main ────────────────────────────────────────────────────── */
|
||
|
||
async function load() {
|
||
var allEvents = [];
|
||
try {
|
||
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>
|