geoproject-app/public/site/templates/narrative-web.php

485 lines
18 KiB
PHP
Raw Normal View History

2026-02-24 19:47:30 +01:00
<?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 ?>
<!-- ═══════════════════════════════════════════
HERO / 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-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->introduction()->isNotEmpty()): ?>
<section class="nw-introduction">
<div class="nw-container nw-container--narrow">
<div class="nw-introduction__body">
<?= $original->introduction() ?>
</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->text()->isNotEmpty()): ?>
<div class="nw-prose"><?= $subpage->text() ?></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->text()->isNotEmpty()): ?>
<div class="nw-map-marker__content">
<?php foreach ($marker->text()->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>