diff --git a/web/assets/style.css b/web/assets/style.css index 3f7ec2c..8d3d2d8 100644 --- a/web/assets/style.css +++ b/web/assets/style.css @@ -331,6 +331,70 @@ td.center { text-align: center; } .ip-bar { background: var(--primary); height: 10px; border-radius: 4px; min-width: 4px; } .ip-count { width: 24px; text-align: right; font-weight: 600; } +/* ── Calendar events ──────────────────────────────────────────────── */ +.calendar-section { margin-bottom: 2.5rem; } +.calendar-section h2 { font-size: 1.2rem; margin-bottom: 1rem; } + +.about-card--event { + cursor: pointer; + transition: border-color .15s, box-shadow .15s; +} +.about-card--event:hover { + border-color: var(--primary); + box-shadow: var(--shadow); +} + +/* ── Event modal ──────────────────────────────────────────────────── */ +.event-modal-overlay { + display: none; + position: fixed; inset: 0; z-index: 200; + background: rgba(0,0,0,.45); + align-items: center; + justify-content: center; + padding: 1rem; +} +.event-modal-overlay--open { display: flex; } + +.event-modal { + background: var(--bg2); + border-radius: var(--radius); + box-shadow: 0 8px 40px rgba(0,0,0,.18); + padding: 2rem; + max-width: 560px; + width: 100%; + position: relative; + max-height: 90vh; + overflow-y: auto; +} + +.event-modal-close { + position: absolute; top: 1rem; right: 1rem; + background: none; border: none; cursor: pointer; + font-size: 1.4rem; color: var(--text-muted); line-height: 1; +} +.event-modal-close:hover { color: var(--text); } + +.event-modal-title { font-size: 1.2rem; margin-bottom: 1rem; padding-right: 2rem; } + +.event-modal-meta { + display: grid; + grid-template-columns: auto 1fr; + gap: .3rem .8rem; + font-size: .88rem; + margin-bottom: 1.2rem; +} +.event-modal-meta dt { color: var(--text-muted); white-space: nowrap; } +.event-modal-meta dd { color: var(--text); } + +.event-modal-desc { + font-size: .9rem; + color: var(--text); + line-height: 1.7; + border-top: 1px solid var(--border); + padding-top: 1rem; + white-space: pre-line; +} + /* ── Footer ───────────────────────────────────────────────────────── */ footer { text-align: center; diff --git a/web/inc/calendar.php b/web/inc/calendar.php index 81820a0..8b8f542 100644 --- a/web/inc/calendar.php +++ b/web/inc/calendar.php @@ -19,26 +19,37 @@ function _ical_upcoming(string $ical, int $n): array { $events = []; $now = time(); + // Unfold continuation lines before parsing + $ical = preg_replace('/\r?\n[ \t]/', '', $ical); + preg_match_all('/BEGIN:VEVENT(.+?)END:VEVENT/s', $ical, $blocks); foreach ($blocks[1] as $b) { $e = []; if (preg_match('/^SUMMARY[^:]*:(.+)$/m', $b, $m)) - $e['title'] = trim($m[1]); + $e['title'] = _ical_unescape(trim($m[1])); if (preg_match('/^LOCATION[^:]*:(.+)$/m', $b, $m)) - $e['location'] = trim($m[1]); + $e['location'] = _ical_unescape(trim($m[1])); + + if (preg_match('/^DESCRIPTION[^:]*:(.+)$/m', $b, $m)) + $e['description'] = _ical_unescape(trim($m[1])); + + if (preg_match('/^URL[^:]*:(.+)$/m', $b, $m)) + $e['url'] = trim($m[1]); // Handles: DTSTART:YYYYMMDD, DTSTART:YYYYMMDDTHHmmss[Z], DTSTART;TZID=...:YYYYMMDDTHHmmss - if (preg_match('/^DTSTART(?:;[^:]+)?:([\dTZ]+)/m', $b, $m)) { - $raw = $m[1]; - if (strlen($raw) === 8) { - $e['start'] = mktime(0, 0, 0, (int)substr($raw, 4, 2), (int)substr($raw, 6, 2), (int)substr($raw, 0, 4)); - } else { - $dt = DateTime::createFromFormat('Ymd\THis\Z', $raw, new DateTimeZone('UTC')) - ?: DateTime::createFromFormat('Ymd\THis', $raw, new DateTimeZone('Europe/Paris')); - if ($dt) $e['start'] = $dt->getTimestamp(); + foreach (['start' => 'DTSTART', 'end' => 'DTEND'] as $key => $prop) { + if (preg_match('/^' . $prop . '(?:;[^:]+)?:([\dTZ]+)/m', $b, $m)) { + $raw = $m[1]; + if (strlen($raw) === 8) { + $e[$key] = mktime(0, 0, 0, (int)substr($raw, 4, 2), (int)substr($raw, 6, 2), (int)substr($raw, 0, 4)); + } else { + $dt = DateTime::createFromFormat('Ymd\THis\Z', $raw, new DateTimeZone('UTC')) + ?: DateTime::createFromFormat('Ymd\THis', $raw, new DateTimeZone('Europe/Paris')); + if ($dt) $e[$key] = $dt->getTimestamp(); + } } } @@ -50,3 +61,7 @@ function _ical_upcoming(string $ical, int $n): array { return array_slice($events, 0, $n); } + +function _ical_unescape(string $s): string { + return str_replace(['\\n', '\\,', '\\;', '\\\\'], ["\n", ',', ';', '\\'], $s); +} diff --git a/web/index.php b/web/index.php index 89781d0..38ae9e5 100644 --- a/web/index.php +++ b/web/index.php @@ -70,13 +70,73 @@ require __DIR__ . '/views/layout.php';
= date('d/m/Y', $ev['start']) ?>= !empty($ev['location']) ? ' — ' . htmlspecialchars($ev['location']) : '' ?>
++ = date('d/m/Y H:i', $ev['start']) ?> + = !empty($ev['location']) ? ' — ' . htmlspecialchars($ev['location']) : '' ?> +