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-bar { background: var(--primary); height: 10px; border-radius: 4px; min-width: 4px; }
|
||||||
.ip-count { width: 24px; text-align: right; font-weight: 600; }
|
.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 ───────────────────────────────────────────────────────── */
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,37 @@ function _ical_upcoming(string $ical, int $n): array {
|
||||||
$events = [];
|
$events = [];
|
||||||
$now = time();
|
$now = time();
|
||||||
|
|
||||||
|
// Unfold continuation lines before parsing
|
||||||
|
$ical = preg_replace('/\r?\n[ \t]/', '', $ical);
|
||||||
|
|
||||||
preg_match_all('/BEGIN:VEVENT(.+?)END:VEVENT/s', $ical, $blocks);
|
preg_match_all('/BEGIN:VEVENT(.+?)END:VEVENT/s', $ical, $blocks);
|
||||||
|
|
||||||
foreach ($blocks[1] as $b) {
|
foreach ($blocks[1] as $b) {
|
||||||
$e = [];
|
$e = [];
|
||||||
|
|
||||||
if (preg_match('/^SUMMARY[^:]*:(.+)$/m', $b, $m))
|
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))
|
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
|
// Handles: DTSTART:YYYYMMDD, DTSTART:YYYYMMDDTHHmmss[Z], DTSTART;TZID=...:YYYYMMDDTHHmmss
|
||||||
if (preg_match('/^DTSTART(?:;[^:]+)?:([\dTZ]+)/m', $b, $m)) {
|
foreach (['start' => 'DTSTART', 'end' => 'DTEND'] as $key => $prop) {
|
||||||
$raw = $m[1];
|
if (preg_match('/^' . $prop . '(?:;[^:]+)?:([\dTZ]+)/m', $b, $m)) {
|
||||||
if (strlen($raw) === 8) {
|
$raw = $m[1];
|
||||||
$e['start'] = mktime(0, 0, 0, (int)substr($raw, 4, 2), (int)substr($raw, 6, 2), (int)substr($raw, 0, 4));
|
if (strlen($raw) === 8) {
|
||||||
} else {
|
$e[$key] = mktime(0, 0, 0, (int)substr($raw, 4, 2), (int)substr($raw, 6, 2), (int)substr($raw, 0, 4));
|
||||||
$dt = DateTime::createFromFormat('Ymd\THis\Z', $raw, new DateTimeZone('UTC'))
|
} else {
|
||||||
?: DateTime::createFromFormat('Ymd\THis', $raw, new DateTimeZone('Europe/Paris'));
|
$dt = DateTime::createFromFormat('Ymd\THis\Z', $raw, new DateTimeZone('UTC'))
|
||||||
if ($dt) $e['start'] = $dt->getTimestamp();
|
?: 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);
|
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>
|
<h2>Prochains événements</h2>
|
||||||
<div class="about-grid">
|
<div class="about-grid">
|
||||||
<?php foreach ($calendar_events as $ev): ?>
|
<?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>
|
<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>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<section class="services-section">
|
<section class="services-section">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue