All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 22s
498 lines
No EOL
18 KiB
PHP
498 lines
No EOL
18 KiB
PHP
<?php
|
||
/**
|
||
* Template: narrative-web.php
|
||
* Affichage web storytelling d'une narrative et de ses sous-pages
|
||
* Structure : narrative > geoformat > chapitre / map > marker
|
||
*/
|
||
|
||
// Collect all subpages for lateral navigation
|
||
$original = page($page->originalUri());
|
||
$subpages = $original->children()->listed();
|
||
$navItems = [];
|
||
|
||
foreach ($subpages as $subpage) {
|
||
$item = [
|
||
'id' => $subpage->uid(),
|
||
'title' => $subpage->title()->value(),
|
||
'template' => $subpage->intendedTemplate()->name(),
|
||
'children' => [],
|
||
];
|
||
// Pour les cartes : inclure les marqueurs comme sous-items de navigation
|
||
if ($subpage->intendedTemplate()->name() === 'map') {
|
||
foreach ($subpage->children()->listed()->filterBy('intendedTemplate', 'marker') as $mk) {
|
||
$item['children'][] = [
|
||
'id' => $mk->uid(),
|
||
'title' => $mk->title()->value(),
|
||
];
|
||
}
|
||
}
|
||
$navItems[] = $item;
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?= e($original->isHomePage() != true, $original->title() . ' – ') . $site->title() ?></title>
|
||
|
||
<!-- Styles globaux existants -->
|
||
<link rel="stylesheet" href="<?= url('assets/css/style.css') ?>">
|
||
<!-- Styles narrative web -->
|
||
<link rel="stylesheet" href="<?= url('assets/css/web.css') ?>">
|
||
|
||
<!-- MapLibre GL -->
|
||
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
|
||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||
|
||
<!-- Custom CSS éventuel depuis le panel -->
|
||
<?php if ($original->customCss()->isNotEmpty()): ?>
|
||
<style><?= $original->customCss() ?></style>
|
||
<?php endif ?>
|
||
|
||
<!-- À SUPPRIMER EN PRODUCTION -->
|
||
<meta name="robots" content="noindex, nofollow, noarchive">
|
||
|
||
<?php if ($kirby->user()): ?>
|
||
<meta name="csrf" content="<?= csrf() ?>">
|
||
<?php endif ?>
|
||
</head>
|
||
|
||
<body data-template="<?= $original->template() ?>">
|
||
|
||
<!-- ═══════════════════════════════════════════
|
||
NAVIGATION LATÉRALE (ancres)
|
||
════════════════════════════════════════════ -->
|
||
<?php if (!empty($navItems)): ?>
|
||
<nav class="nw-sidenav" aria-label="Navigation sections">
|
||
<ul class="nw-sidenav__list">
|
||
<li class="nw-sidenav__item nw-sidenav__item--intro">
|
||
<a class="nw-sidenav__link" href="#nw-intro">
|
||
<span class="nw-sidenav__dot"></span>
|
||
<span class="nw-sidenav__label">Introduction</span>
|
||
</a>
|
||
</li>
|
||
<?php foreach ($navItems as $item): ?>
|
||
<li class="nw-sidenav__item nw-sidenav__item--<?= $item['template'] ?>">
|
||
<a class="nw-sidenav__link" href="#section-<?= $item['id'] ?>">
|
||
<span class="nw-sidenav__dot"></span>
|
||
<span class="nw-sidenav__label"><?= html($item['title']) ?></span>
|
||
</a>
|
||
<?php if (!empty($item['children'])): ?>
|
||
<ul class="nw-sidenav__sub">
|
||
<?php foreach ($item['children'] as $child): ?>
|
||
<li class="nw-sidenav__sub-item">
|
||
<a class="nw-sidenav__sub-link" href="#marker-<?= $child['id'] ?>">
|
||
<span class="nw-sidenav__sub-dot"></span>
|
||
<span class="nw-sidenav__sub-label"><?= html($child['title']) ?></span>
|
||
</a>
|
||
</li>
|
||
<?php endforeach ?>
|
||
</ul>
|
||
<?php endif ?>
|
||
</li>
|
||
<?php endforeach ?>
|
||
</ul>
|
||
</nav>
|
||
<?php endif ?>
|
||
|
||
<!-- ═══════════════════════════════════════════
|
||
BANDEAU / INTRODUCTION
|
||
════════════════════════════════════════════ -->
|
||
<header class="nw-hero" id="nw-intro">
|
||
<?php if ($original->cover()->isNotEmpty() && $cover = $original->cover()->toFile()): ?>
|
||
<div class="nw-hero__bg" style="background-image: url('<?= $cover->url() ?>')"></div>
|
||
<div class="nw-hero__overlay"></div>
|
||
<?php endif ?>
|
||
|
||
<div class="nw-prev-btn">
|
||
<a href="<?= $original->parent()->url()?>" title="<?= $original->parent()->title()?>">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
||
fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M19 12H5"/>
|
||
<polyline points="12 19 5 12 12 5"/>
|
||
</svg>
|
||
<?= $original->parent()->title()->html()?>
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="nw-hero__content">
|
||
<h1 class="nw-hero__title"><?= html($original->title()) ?></h1>
|
||
<?php if ($original->author()->isNotEmpty()): ?>
|
||
<p class="nw-hero__author">Par <span><?= html($original->author()) ?></span></p>
|
||
<?php endif ?>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Introduction -->
|
||
<?php if ($original->intro()->isNotEmpty()): ?>
|
||
<section class="nw-introduction">
|
||
<div class="nw-container nw-container--narrow">
|
||
<div class="nw-introduction__body">
|
||
<?= $original->intro() ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<?php endif ?>
|
||
|
||
<!-- ═══════════════════════════════════════════
|
||
SOUS-PAGES
|
||
════════════════════════════════════════════ -->
|
||
<main class="nw-main">
|
||
<?php foreach ($subpages as $subpage):
|
||
$tpl = $subpage->intendedTemplate()->name();
|
||
?>
|
||
|
||
<!-- ─────────────────────────────────────────
|
||
CARTE (map.yaml > marker.yaml)
|
||
───────────────────────────────────────── -->
|
||
<?php if ($tpl === 'map'): ?>
|
||
<section class="nw-section nw-section--map" id="section-<?= $subpage->uid() ?>">
|
||
<div class="nw-container">
|
||
<header class="nw-section__header">
|
||
<h2 class="nw-section__title"><?= html($subpage->title()) ?></h2>
|
||
<?php if ($subpage->tags()->isNotEmpty()): ?>
|
||
<div class="nw-tags">
|
||
<?php foreach ($subpage->tags()->split(',') as $tag): ?>
|
||
<span class="nw-tag"><?= html(trim($tag)) ?></span>
|
||
<?php endforeach ?>
|
||
</div>
|
||
<?php endif ?>
|
||
</header>
|
||
|
||
<?php if ($subpage->intro()->isNotEmpty()): ?>
|
||
<div class="nw-prose"><?= $subpage->intro() ?></div>
|
||
<?php endif ?>
|
||
</div>
|
||
|
||
<!-- Carte MapLibre inline -->
|
||
<?php
|
||
$markers = $subpage->children()->listed()->filterBy('intendedTemplate', 'marker');
|
||
$mapId = 'map-' . $subpage->uid();
|
||
?>
|
||
<div class="nw-map-wrap">
|
||
<div id="<?= $mapId ?>" class="nw-map"></div>
|
||
</div>
|
||
|
||
<!-- Fichiers de la carte -->
|
||
<?php if ($subpage->files()->isNotEmpty()): ?>
|
||
<div class="nw-container">
|
||
<div class="nw-files">
|
||
<?php foreach ($subpage->files() as $file): ?>
|
||
<a href="<?= $file->url() ?>" class="nw-file" target="_blank">
|
||
<span class="nw-file__icon">↓</span>
|
||
<span class="nw-file__name"><?= html($file->filename()) ?></span>
|
||
</a>
|
||
<?php endforeach ?>
|
||
</div>
|
||
</div>
|
||
<?php endif ?>
|
||
|
||
<!-- ── Liste des marqueurs sous la carte ── -->
|
||
<?php if ($markers->isNotEmpty()): ?>
|
||
<div class="nw-map-marker-list nw-container">
|
||
<?php $mNum = 1; foreach ($markers as $marker): ?>
|
||
<article
|
||
class="nw-map-marker"
|
||
id="marker-<?= $marker->uid() ?>"
|
||
data-lat="<?= $marker->latitude()->value() ?>"
|
||
data-lng="<?= $marker->longitude()->value() ?>"
|
||
>
|
||
<?php if ($marker->cover()->isNotEmpty() && $mCover = $marker->cover()->toFile()): ?>
|
||
<div class="nw-map-marker__cover">
|
||
<img src="<?= $mCover->url() ?>" alt="<?= html($marker->title()) ?>">
|
||
<span class="nw-map-marker__num"><?= $mNum ?></span>
|
||
</div>
|
||
<?php else: ?>
|
||
<span class="nw-map-marker__num nw-map-marker__num--no-cover"><?= $mNum ?></span>
|
||
<?php endif ?>
|
||
|
||
<div class="nw-map-marker__body">
|
||
<h3 class="nw-map-marker__title"><?= html($marker->title()) ?></h3>
|
||
<?php if ($marker->body()->isNotEmpty()): ?>
|
||
<div class="nw-map-marker__content">
|
||
<?php foreach ($marker->body()->toBlocks() as $block): ?>
|
||
<div class="block block-type-<?= $block->type() ?>">
|
||
<?= $block ?>
|
||
</div>
|
||
<?php endforeach ?>
|
||
</div>
|
||
<?php endif ?>
|
||
</div>
|
||
</article>
|
||
<?php $mNum++; endforeach ?>
|
||
</div>
|
||
<?php endif ?>
|
||
|
||
<script>
|
||
(function() {
|
||
var markers = <?php
|
||
$mData = [];
|
||
foreach ($markers as $m) {
|
||
if ($m->latitude()->isNotEmpty() && $m->longitude()->isNotEmpty()) {
|
||
$coverUrl = '';
|
||
if ($m->cover()->isNotEmpty() && $mCov = $m->cover()->toFile()) {
|
||
$coverUrl = $mCov->url();
|
||
}
|
||
$iconUrl = '';
|
||
$iconSize = (int)$m->markerIconSize()->or(40)->value();
|
||
if ($m->markerIcon()->isNotEmpty() && $icon = $m->markerIcon()->toFile()) {
|
||
$iconUrl = $icon->url();
|
||
}
|
||
$mData[] = [
|
||
'uid' => $m->uid(),
|
||
'lat' => (float)$m->latitude()->value(),
|
||
'lng' => (float)$m->longitude()->value(),
|
||
'title' => $m->title()->value(),
|
||
'coverUrl' => $coverUrl,
|
||
'iconUrl' => $iconUrl,
|
||
'iconSize' => $iconSize,
|
||
];
|
||
}
|
||
}
|
||
echo json_encode($mData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
?>;
|
||
|
||
// Centre de la carte
|
||
var mapdata = <?php
|
||
$defaultCenter = [43.836699, 4.360054];
|
||
$defaultZoom = 13;
|
||
if ($subpage->mapdata()->isNotEmpty()) {
|
||
$raw = $subpage->mapdata()->value();
|
||
$decoded = json_decode($raw, true);
|
||
if ($decoded) {
|
||
echo json_encode($decoded);
|
||
} else {
|
||
echo json_encode(['center' => $defaultCenter, 'zoom' => $defaultZoom]);
|
||
}
|
||
} else {
|
||
echo json_encode(['center' => $defaultCenter, 'zoom' => $defaultZoom]);
|
||
}
|
||
?>;
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var center = mapdata.center || [<?= $defaultCenter[1] ?>, <?= $defaultCenter[0] ?>];
|
||
var zoom = mapdata.zoom || 13;
|
||
|
||
// MapLibre attend [lng, lat]
|
||
if (Array.isArray(center) && center.length === 2) {
|
||
// Si stocké [lat, lng], inverser
|
||
if (center[0] > 90 || center[0] < -90) {
|
||
// déjà [lng, lat]
|
||
} else {
|
||
center = [center[1], center[0]];
|
||
}
|
||
}
|
||
|
||
var map = new maplibregl.Map({
|
||
container: '<?= $mapId ?>',
|
||
style: 'https://api.maptiler.com/maps/dataviz/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
|
||
center: center,
|
||
zoom: zoom,
|
||
attributionControl: true
|
||
});
|
||
|
||
// Fallback style OSM si pas de clé MapTiler
|
||
map.on('error', function() {
|
||
map.setStyle({
|
||
version: 8,
|
||
sources: {
|
||
osm: {
|
||
type: 'raster',
|
||
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||
tileSize: 256,
|
||
attribution: '© OpenStreetMap contributors'
|
||
}
|
||
},
|
||
layers: [{ id: 'osm', type: 'raster', source: 'osm' }]
|
||
});
|
||
});
|
||
|
||
markers.forEach(function(m) {
|
||
var el = null;
|
||
if (m.iconUrl) {
|
||
el = document.createElement('div');
|
||
el.className = 'nw-marker-icon';
|
||
el.style.backgroundImage = 'url(' + m.iconUrl + ')';
|
||
el.style.width = m.iconSize + 'px';
|
||
el.style.height = m.iconSize + 'px';
|
||
}
|
||
|
||
var markerOpts = { color: 'var(--nw-accent, #c0392b)' };
|
||
if (el) markerOpts.element = el;
|
||
|
||
// Construire le HTML de la popup : cover + titre + ancre vers le texte
|
||
var popupHtml = '';
|
||
if (m.coverUrl) {
|
||
popupHtml += '<div class="nw-popup-cover"><img src="' + m.coverUrl + '" alt="' + m.title + '"></div>';
|
||
}
|
||
popupHtml += '<div class="nw-popup-body">';
|
||
if (m.title) {
|
||
popupHtml += '<strong class="nw-popup-title">' + m.title + '</strong>';
|
||
}
|
||
popupHtml += '<a class="nw-popup-anchor" href="#marker-' + m.uid + '">Lire la fiche ↓</a>';
|
||
popupHtml += '</div>';
|
||
|
||
var marker = new maplibregl.Marker(markerOpts)
|
||
.setLngLat([m.lng, m.lat])
|
||
.setPopup(new maplibregl.Popup({ offset: 25, maxWidth: '280px' }).setHTML(popupHtml))
|
||
.addTo(map);
|
||
});
|
||
|
||
// Adapter les bounds si on a des markers
|
||
if (markers.length > 1) {
|
||
var bounds = new maplibregl.LngLatBounds();
|
||
markers.forEach(function(m) { bounds.extend([m.lng, m.lat]); });
|
||
map.fitBounds(bounds, { padding: 60, maxZoom: 16 });
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
</section>
|
||
|
||
<!-- ─────────────────────────────────────────
|
||
GÉOFORMAT (geoformat.yaml > chapitre.yaml)
|
||
───────────────────────────────────────── -->
|
||
<?php elseif ($tpl === 'geoformat'): ?>
|
||
<section class="nw-section nw-section--geoformat" id="section-<?= $subpage->uid() ?>">
|
||
|
||
<!-- En-tête géoformat -->
|
||
<div class="nw-geoformat-hero">
|
||
<?php if ($subpage->cover()->isNotEmpty() && $gCover = $subpage->cover()->toFile()): ?>
|
||
<div class="nw-geoformat-hero__bg" style="background-image: url('<?= $gCover->url() ?>')"></div>
|
||
<div class="nw-geoformat-hero__overlay"></div>
|
||
<?php endif ?>
|
||
<div class="nw-geoformat-hero__content nw-container">
|
||
<h2 class="nw-geoformat-hero__title"><?= html($subpage->title()) ?></h2>
|
||
<?php if ($subpage->subtitle()->isNotEmpty()): ?>
|
||
<p class="nw-geoformat-hero__subtitle"><?= html($subpage->subtitle()) ?></p>
|
||
<?php endif ?>
|
||
<?php if ($subpage->tags()->isNotEmpty()): ?>
|
||
<div class="nw-tags">
|
||
<?php foreach ($subpage->tags()->split(',') as $tag): ?>
|
||
<span class="nw-tag"><?= html(trim($tag)) ?></span>
|
||
<?php endforeach ?>
|
||
</div>
|
||
<?php endif ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chapeau -->
|
||
<?php if ($subpage->text()->isNotEmpty()): ?>
|
||
<div class="nw-container nw-container--narrow">
|
||
<div class="nw-chapeau"><?= $subpage->text() ?></div>
|
||
</div>
|
||
<?php endif ?>
|
||
|
||
<!-- Chapitres -->
|
||
<?php
|
||
$chapitres = $subpage->children()->listed()->filterBy('intendedTemplate', 'chapitre');
|
||
$chapCount = $chapitres->count();
|
||
if ($chapCount > 0): ?>
|
||
|
||
<!-- Navigation chapitres locale -->
|
||
<?php if ($chapCount > 1): ?>
|
||
<div class="nw-container">
|
||
<nav class="nw-chap-nav" aria-label="Chapitres">
|
||
<?php $ci = 1; foreach ($chapitres as $chap): ?>
|
||
<a class="nw-chap-nav__link" href="#chap-<?= $chap->uid() ?>">
|
||
<span class="nw-chap-nav__num"><?= $ci ?></span>
|
||
<span class="nw-chap-nav__title"><?= html($chap->title()) ?></span>
|
||
</a>
|
||
<?php $ci++; endforeach ?>
|
||
</nav>
|
||
</div>
|
||
<?php endif ?>
|
||
|
||
<?php $chapNum = 1; foreach ($chapitres as $chap): ?>
|
||
<article class="nw-chapitre" id="chap-<?= $chap->uid() ?>">
|
||
<div class="nw-container nw-container--narrow">
|
||
<header class="nw-chapitre__header">
|
||
<span class="nw-chapitre__num">Chapitre <?= $chapNum ?></span>
|
||
<h3 class="nw-chapitre__title"><?= html($chap->title()) ?></h3>
|
||
</header>
|
||
<div class="nw-chapitre__body">
|
||
<?= $chap->text()->toBlocks() ?>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<?php $chapNum++; endforeach ?>
|
||
<?php endif ?>
|
||
|
||
</section>
|
||
<?php endif ?>
|
||
|
||
<?php endforeach ?>
|
||
</main>
|
||
|
||
<!-- ═══════════════════════════════════════════
|
||
FOOTER
|
||
════════════════════════════════════════════ -->
|
||
<?php snippet('footer') ?>
|
||
|
||
<!-- Script animations sections -->
|
||
<script>
|
||
(function() {
|
||
var sections = document.querySelectorAll('.nw-section');
|
||
if (!sections.length) return;
|
||
var io = new IntersectionObserver(function(entries) {
|
||
entries.forEach(function(e) {
|
||
if (e.isIntersecting) {
|
||
e.target.classList.add('is-visible');
|
||
io.unobserve(e.target);
|
||
}
|
||
});
|
||
}, { threshold: 0.08 });
|
||
sections.forEach(function(s) { io.observe(s); });
|
||
})();
|
||
</script>
|
||
|
||
<!-- Script navigation latérale (active state au scroll) -->
|
||
<script>
|
||
(function() {
|
||
var links = document.querySelectorAll('.nw-sidenav__link');
|
||
var subLinks = document.querySelectorAll('.nw-sidenav__sub-link');
|
||
var targets = document.querySelectorAll('[id^="section-"], #nw-intro');
|
||
var markerTargets = document.querySelectorAll('[id^="marker-"]');
|
||
if (!links.length || !targets.length) return;
|
||
|
||
// Observer sections principales
|
||
var observer = new IntersectionObserver(function(entries) {
|
||
entries.forEach(function(entry) {
|
||
if (entry.isIntersecting) {
|
||
links.forEach(function(l) { l.classList.remove('is-active'); });
|
||
document.querySelectorAll('.nw-sidenav__item').forEach(function(li) {
|
||
li.classList.remove('is-parent-active');
|
||
});
|
||
var active = document.querySelector('.nw-sidenav__link[href="#' + entry.target.id + '"]');
|
||
if (active) {
|
||
active.classList.add('is-active');
|
||
var parentLi = active.closest('.nw-sidenav__item');
|
||
if (parentLi) parentLi.classList.add('is-parent-active');
|
||
}
|
||
}
|
||
});
|
||
}, { rootMargin: '-20% 0px -70% 0px' });
|
||
|
||
targets.forEach(function(t) { observer.observe(t); });
|
||
|
||
// Observer marqueurs pour les sous-liens
|
||
if (markerTargets.length && subLinks.length) {
|
||
var subObserver = new IntersectionObserver(function(entries) {
|
||
entries.forEach(function(entry) {
|
||
if (entry.isIntersecting) {
|
||
subLinks.forEach(function(l) { l.classList.remove('is-active'); });
|
||
var active = document.querySelector('.nw-sidenav__sub-link[href="#' + entry.target.id + '"]');
|
||
if (active) active.classList.add('is-active');
|
||
}
|
||
});
|
||
}, { rootMargin: '-15% 0px -65% 0px' });
|
||
markerTargets.forEach(function(t) { subObserver.observe(t); });
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|