alpinux.site.2026/home/index.html
Cédrix a580d98518 feat: widget événements dynamiques depuis le calendrier Nextcloud
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 10:05:34 +02:00

440 lines
14 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;
}
/* ── 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;
}
/* ── Events ─────────────────────────────────────────────────── */
.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;
}
.events {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
margin-bottom: 3rem;
}
.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;
}
.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; }
/* ── 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>
<header>
<div class="logo-wrap">
<img src="https://static.alpinux.org/logo/alpinux-logo.png"
alt="Logo Alpinux — LUG de Savoie"
class="logo-img"
width="90" height="90">
<div class="logo-text">
<div class="brand">
<span class="light">A</span><span class="bold">l</span><span class="light">p</span><span class="bold">inux</span>
</div>
<div class="tagline">le LUG de Savoie</div>
</div>
</div>
<p class="hero-text">
Association loi 1901 dédiée à <strong>Linux et aux logiciels libres</strong> en Savoie.
Réunions tous les 1<sup>er</sup> et 3<sup>e</sup> jeudis du mois à la <strong>Dynamo Chambéry</strong>.
</p>
</header>
<nav aria-label="Navigation principale">
<a href="https://wiki.alpinux.org">Wiki</a>
<a href="https://portail.alpinux.org">Portail membres</a>
<a href="https://installparty.alpinux.org">Install Party</a>
<a href="https://gitea.alpinux.org">Gitea</a>
<a href="https://mamot.fr/@alpinux" rel="me">Mastodon</a>
<a href="https://www.helloasso.com/associations/alpinux-le-lug-de-savoie">Adhérer</a>
</nav>
<main>
<h2 class="section-title">Prochains événements</h2>
<div class="events" id="events-container">
<div class="event-card" id="static-reunion">
<div class="event-date">1<sup>er</sup> &amp; 3<sup>e</sup> jeudis</div>
<div class="event-title">Réunion mensuelle</div>
<div class="event-where">📍 Dynamo Chambéry — 18h00</div>
</div>
<div class="event-card" id="static-installparty">
<div class="event-date">Voir le calendrier</div>
<div class="event-title">Install Party &amp; ateliers</div>
<div class="event-where">📅 <a href="https://installparty.alpinux.org" style="color:#1a6bbf;">installparty.alpinux.org</a></div>
</div>
</div>
<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://dynamic.alpinux.org" class="service-card">
<div class="service-icon">🎮</div>
<div class="service-name">Jeux &amp; quiz</div>
<div class="service-desc">Mini-jeux et questionnaires interactifs</div>
</a>
</div>
</main>
<footer>
<p>© Alpinux — Association loi 1901 &nbsp;·&nbsp; Chambéry, Savoie</p>
<p style="margin-top:.4rem;">
<a href="https://wiki.alpinux.org/alpinux/">À propos</a> &nbsp;·&nbsp;
<a href="https://wiki.alpinux.org/alpinux/faq/">FAQ</a> &nbsp;·&nbsp;
<a href="https://mamot.fr/@alpinux" rel="me">Mastodon</a> &nbsp;·&nbsp;
<a href="https://www.helloasso.com/associations/alpinux-le-lug-de-savoie">Adhérer</a>
</p>
</footer>
<script>
(function () {
const CAL_URL = '/public-calendars/n5BWPYsxw7FCYozM';
function unfold(ics) {
return ics.replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, '');
}
function parseDate(line) {
const ci = line.indexOf(':');
const params = line.substring(0, ci).toUpperCase();
const val = line.substring(ci + 1).trim();
const y = +val.substring(0, 4), mo = +val.substring(4, 6) - 1, d = +val.substring(6, 8);
if (params.includes('VALUE=DATE') || /^\d{8}$/.test(val))
return { date: new Date(y, mo, d), allDay: true };
const h = +val.substring(9, 11), mi = +val.substring(11, 13), s = +val.substring(13, 15);
const dt = val.endsWith('Z')
? new Date(Date.UTC(y, mo, d, h, mi, s))
: new Date(y, mo, d, h, mi, s);
return { date: dt, allDay: false };
}
function parseICS(text) {
const lines = unfold(text).split(/\r?\n/);
const events = [], cap = /^(.+?)(;[^:]*)?:(.*)$/;
let cur = null;
for (const line of lines) {
if (line === 'BEGIN:VEVENT') { cur = {}; continue; }
if (line === 'END:VEVENT') { if (cur && cur.start) events.push(cur); cur = null; continue; }
if (!cur) continue;
const m = cap.exec(line);
if (!m) continue;
const key = m[1].toUpperCase();
const val = m[3];
if (key === 'SUMMARY') cur.title = val.replace(/\\,/g, ',').replace(/\\n/g, ' ');
if (key === 'LOCATION') cur.location = val.replace(/\\,/g, ',');
if (key === 'DTSTART') cur.start = parseDate(line);
}
return events;
}
function fmtDate(d, allDay) {
const opts = { weekday: 'long', day: 'numeric', month: 'long' };
if (!allDay) opts.hour = '2-digit', opts.minute = '2-digit';
return d.toLocaleDateString('fr-FR', opts);
}
function makeCard(ev) {
const card = document.createElement('div');
card.className = 'event-card';
card.innerHTML =
'<div class="event-date">' + fmtDate(ev.start.date, ev.start.allDay) + '</div>' +
'<div class="event-title">' + ev.title + '</div>' +
(ev.location ? '<div class="event-where">📍 ' + ev.location + '</div>' : '');
return card;
}
async function load() {
try {
const r = await fetch(CAL_URL);
if (!r.ok) return;
const text = await r.text();
if (!text.trim()) return;
const now = new Date();
now.setHours(0, 0, 0, 0);
const upcoming = parseICS(text)
.filter(e => e.start.date >= now)
.sort((a, b) => a.start.date - b.start.date)
.slice(0, 6);
if (!upcoming.length) return;
const container = document.getElementById('events-container');
const anchor = document.getElementById('static-installparty');
const staticReunion = document.getElementById('static-reunion');
if (staticReunion) staticReunion.remove();
upcoming.forEach(ev => container.insertBefore(makeCard(ev), anchor));
} catch (_) { /* silently keep static tiles */ }
}
document.addEventListener('DOMContentLoaded', load);
})();
</script>
</body>
</html>