Modale de détail au clic sur une tuile événement (#2)
- calendar.php : ajout DTEND, DESCRIPTION, URL ; dépliage des lignes iCal et désescapage (\n, \,, \;) - index.php : tuiles cliquables avec data-event JSON, modale native (overlay + dialog div), JS vanilla pour ouverture/fermeture (clic extérieur, touche Escape) - style.css : styles modale et hover sur tuiles événements Closes #2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ce9c3ad3b
commit
fc4d010c66
3 changed files with 151 additions and 12 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,13 +70,73 @@ require __DIR__ . '/views/layout.php';
|
|||
<h2>Prochains événements</h2>
|
||||
<div class="about-grid">
|
||||
<?php foreach ($calendar_events as $ev): ?>
|
||||
<div class="about-card">
|
||||
<div class="about-card about-card--event"
|
||||
onclick="openEventModal(this)"
|
||||
data-event="<?= htmlspecialchars(json_encode([
|
||||
'title' => $ev['title'],
|
||||
'start' => $ev['start'],
|
||||
'end' => $ev['end'] ?? null,
|
||||
'location' => $ev['location'] ?? null,
|
||||
'description' => $ev['description'] ?? null,
|
||||
'url' => $ev['url'] ?? null,
|
||||
]), ENT_QUOTES) ?>">
|
||||
<div class="about-title"><?= htmlspecialchars($ev['title']) ?></div>
|
||||
<p><?= date('d/m/Y', $ev['start']) ?><?= !empty($ev['location']) ? ' — ' . htmlspecialchars($ev['location']) : '' ?></p>
|
||||
<p>
|
||||
<?= date('d/m/Y H:i', $ev['start']) ?>
|
||||
<?= !empty($ev['location']) ? ' — ' . htmlspecialchars($ev['location']) : '' ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="event-modal-overlay" class="event-modal-overlay" onclick="closeEventModal(event)">
|
||||
<div class="event-modal" role="dialog" aria-modal="true">
|
||||
<button class="event-modal-close" onclick="closeEventModal()" aria-label="Fermer">×</button>
|
||||
<h2 class="event-modal-title" id="event-modal-title"></h2>
|
||||
<dl class="event-modal-meta" id="event-modal-meta"></dl>
|
||||
<div class="event-modal-desc" id="event-modal-desc"></div>
|
||||
<div id="event-modal-url"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openEventModal(card) {
|
||||
const ev = JSON.parse(card.dataset.event);
|
||||
const fmt = ts => ts ? new Date(ts * 1000).toLocaleString('fr-FR', {weekday:'long', day:'numeric', month:'long', year:'numeric', hour:'2-digit', minute:'2-digit'}) : null;
|
||||
|
||||
document.getElementById('event-modal-title').textContent = ev.title;
|
||||
|
||||
let meta = '';
|
||||
const start = fmt(ev.start), end = fmt(ev.end);
|
||||
if (start) meta += `<dt>Début</dt><dd>${start}</dd>`;
|
||||
if (end) meta += `<dt>Fin</dt><dd>${end}</dd>`;
|
||||
if (ev.location) meta += `<dt>Lieu</dt><dd>${ev.location}</dd>`;
|
||||
document.getElementById('event-modal-meta').innerHTML = meta;
|
||||
|
||||
const desc = document.getElementById('event-modal-desc');
|
||||
if (ev.description) {
|
||||
desc.innerHTML = ev.description.replace(/\n/g, '<br>');
|
||||
desc.style.display = '';
|
||||
} else {
|
||||
desc.style.display = 'none';
|
||||
}
|
||||
|
||||
const urlEl = document.getElementById('event-modal-url');
|
||||
urlEl.innerHTML = ev.url ? `<a href="${ev.url}" target="_blank" rel="noopener" class="btn-outline btn-sm" style="margin-top:1rem;display:inline-block">En savoir plus →</a>` : '';
|
||||
|
||||
document.getElementById('event-modal-overlay').classList.add('event-modal-overlay--open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeEventModal(e) {
|
||||
if (e && e.target !== document.getElementById('event-modal-overlay')) return;
|
||||
document.getElementById('event-modal-overlay').classList.remove('event-modal-overlay--open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeEventModal(); });
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="services-section">
|
||||
|
|
|
|||
Loading…
Reference in a new issue