feat: render full map content (image, intro, markers) in PagedJS
- Fix template check 'carte' → 'map' so map subpages are served by API - Add parseMarker() and enrich parseCarte() with static image, intro, markers - Include map children in parseGeoformat() alongside chapters - Resolve map block references in chapters to full carte data - Update narrative store flattening with new carte fields - Replace MapBlock placeholder with full carte rendering (title, image, tags, intro, markers with icons and blocks) - Add default marker-pin.svg for markers without custom icon Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9d80845335
commit
41fbe71a1f
5 changed files with 171 additions and 22 deletions
1
public/assets/svg/marker-pin.svg
Normal file
1
public/assets/svg/marker-pin.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
|
||||||
|
After Width: | Height: | Size: 236 B |
|
|
@ -106,9 +106,14 @@ function parseBlocks($blocksField, $page) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'map':
|
case 'map':
|
||||||
|
$mapPage = $block->map()->toPages()->first();
|
||||||
|
if ($mapPage) {
|
||||||
|
$blockData['content'] = parseCarte($mapPage);
|
||||||
|
} else {
|
||||||
$blockData['content'] = [
|
$blockData['content'] = [
|
||||||
'map' => $block->map()->value()
|
'map' => $block->map()->value()
|
||||||
];
|
];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -135,10 +140,30 @@ function parseChapter($chapter) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse un marqueur
|
||||||
|
*/
|
||||||
|
function parseMarker($marker) {
|
||||||
|
return [
|
||||||
|
'title' => $marker->title()->value(),
|
||||||
|
'cover' => resolveFileUrl($marker->cover(), $marker),
|
||||||
|
'icon' => $marker->markerIcon()->toFile() ? $marker->markerIcon()->toFile()->url() : null,
|
||||||
|
'iconSize' => $marker->markerIconSize()->value() ?? 40,
|
||||||
|
'blocks' => parseBlocks($marker->text(), $marker)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse une carte
|
* Parse une carte
|
||||||
*/
|
*/
|
||||||
function parseCarte($carte) {
|
function parseCarte($carte) {
|
||||||
|
$markers = [];
|
||||||
|
foreach ($carte->children()->listed() as $child) {
|
||||||
|
if ($child->intendedTemplate()->name() === 'marker') {
|
||||||
|
$markers[] = parseMarker($child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$staticImage = $carte->file('map-static.png');
|
||||||
return [
|
return [
|
||||||
'id' => $carte->id(),
|
'id' => $carte->id(),
|
||||||
'uuid' => $carte->uuid()->toString(),
|
'uuid' => $carte->uuid()->toString(),
|
||||||
|
|
@ -146,7 +171,9 @@ function parseCarte($carte) {
|
||||||
'title' => $carte->title()->value(),
|
'title' => $carte->title()->value(),
|
||||||
'slug' => $carte->slug(),
|
'slug' => $carte->slug(),
|
||||||
'tags' => $carte->tags()->isNotEmpty() ? $carte->tags()->split() : [],
|
'tags' => $carte->tags()->isNotEmpty() ? $carte->tags()->split() : [],
|
||||||
'text' => resolveImagesInHtml($carte->text()->value(), $carte)
|
'image' => $staticImage ? $staticImage->url() : null,
|
||||||
|
'intro' => resolveImagesInHtml($carte->text()->value(), $carte),
|
||||||
|
'markers' => $markers
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,10 +181,13 @@ function parseCarte($carte) {
|
||||||
* Parse un geoformat
|
* Parse un geoformat
|
||||||
*/
|
*/
|
||||||
function parseGeoformat($geoformat) {
|
function parseGeoformat($geoformat) {
|
||||||
$chapters = [];
|
$children = [];
|
||||||
foreach ($geoformat->children()->listed() as $child) {
|
foreach ($geoformat->children()->listed() as $child) {
|
||||||
if ($child->intendedTemplate()->name() === 'chapter') {
|
$template = $child->intendedTemplate()->name();
|
||||||
$chapters[] = parseChapter($child);
|
if ($template === 'chapter') {
|
||||||
|
$children[] = parseChapter($child);
|
||||||
|
} elseif ($template === 'map') {
|
||||||
|
$children[] = parseCarte($child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +201,7 @@ function parseGeoformat($geoformat) {
|
||||||
'tags' => $geoformat->tags()->isNotEmpty() ? $geoformat->tags()->split() : [],
|
'tags' => $geoformat->tags()->isNotEmpty() ? $geoformat->tags()->split() : [],
|
||||||
'cover' => resolveFileUrl($geoformat->cover(), $geoformat),
|
'cover' => resolveFileUrl($geoformat->cover(), $geoformat),
|
||||||
'text' => resolveImagesInHtml($geoformat->text()->value(), $geoformat),
|
'text' => resolveImagesInHtml($geoformat->text()->value(), $geoformat),
|
||||||
'children' => $chapters
|
'children' => $children
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,7 +227,7 @@ foreach ($page->children()->listed() as $child) {
|
||||||
|
|
||||||
if ($template === 'geoformat') {
|
if ($template === 'geoformat') {
|
||||||
$data['children'][] = parseGeoformat($child);
|
$data['children'][] = parseGeoformat($child);
|
||||||
} elseif ($template === 'carte') {
|
} elseif ($template === 'map') {
|
||||||
$data['children'][] = parseCarte($child);
|
$data['children'][] = parseCarte($child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,41 @@
|
||||||
class="carte"
|
class="carte"
|
||||||
:data-page-type="item.template"
|
:data-page-type="item.template"
|
||||||
>
|
>
|
||||||
<h2>{{ item.title }}</h2>
|
<h4>{{ item.title }}</h4>
|
||||||
|
<img v-if="item.image" :src="item.image" class="carte-image" alt="" />
|
||||||
<div v-if="item.tags && item.tags.length" class="tags">
|
<div v-if="item.tags && item.tags.length" class="tags">
|
||||||
<span v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</span>
|
<span v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.text" class="carte-content" v-html="item.text"></div>
|
<div v-if="item.intro" class="intro" v-html="item.intro"></div>
|
||||||
|
<div v-if="item.markers && item.markers.length" class="markers">
|
||||||
|
<div v-for="(marker, idx) in item.markers" :key="idx" class="marker">
|
||||||
|
<h5 class="marker-title">
|
||||||
|
<img
|
||||||
|
v-if="marker.icon"
|
||||||
|
:src="marker.icon"
|
||||||
|
class="marker-icon"
|
||||||
|
:style="{ width: marker.iconSize + 'px', height: marker.iconSize + 'px' }"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="/assets/svg/marker-pin.svg"
|
||||||
|
class="marker-icon marker-icon--default"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{{ marker.title }}
|
||||||
|
</h5>
|
||||||
|
<img v-if="marker.cover" :src="marker.cover" class="marker-cover" alt="" />
|
||||||
|
<template v-if="marker.blocks">
|
||||||
|
<component
|
||||||
|
v-for="block in visibleBlocks(marker.blocks)"
|
||||||
|
:key="block.id"
|
||||||
|
:is="getBlockComponent(block.type)"
|
||||||
|
:content="block.content"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -186,9 +216,37 @@ const getBlockComponent = (type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-map {
|
.block-map {
|
||||||
background: #f5f5f5;
|
break-before: page;
|
||||||
padding: 2rem;
|
}
|
||||||
text-align: center;
|
|
||||||
border: 1px dashed #ccc;
|
.carte-image {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-icon--default {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cover {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,72 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="block-map">
|
<div v-if="content.template === 'carte'" class="block-map">
|
||||||
|
<h4>{{ content.title }}</h4>
|
||||||
|
<img v-if="content.image" :src="content.image" class="carte-image" alt="" />
|
||||||
|
<div v-if="content.tags && content.tags.length" class="tags">
|
||||||
|
<span v-for="tag in content.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="content.intro" class="intro" v-html="content.intro"></div>
|
||||||
|
<div v-if="content.markers && content.markers.length" class="markers">
|
||||||
|
<div v-for="(marker, idx) in content.markers" :key="idx" class="marker">
|
||||||
|
<h5 class="marker-title">
|
||||||
|
<img
|
||||||
|
v-if="marker.icon"
|
||||||
|
:src="marker.icon"
|
||||||
|
class="marker-icon"
|
||||||
|
:style="{ width: marker.iconSize + 'px', height: marker.iconSize + 'px' }"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="/assets/svg/marker-pin.svg"
|
||||||
|
class="marker-icon marker-icon--default"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{{ marker.title }}
|
||||||
|
</h5>
|
||||||
|
<img v-if="marker.cover" :src="marker.cover" class="marker-cover" alt="" />
|
||||||
|
<template v-if="marker.blocks">
|
||||||
|
<component
|
||||||
|
v-for="block in visibleBlocks(marker.blocks)"
|
||||||
|
:key="block.id"
|
||||||
|
:is="getBlockComponent(block.type)"
|
||||||
|
:content="block.content"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="block-map">
|
||||||
<p class="map-placeholder">[Carte: {{ content.map }}]</p>
|
<p class="map-placeholder">[Carte: {{ content.map }}]</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import TextBlock from './TextBlock.vue';
|
||||||
|
import HeadingBlock from './HeadingBlock.vue';
|
||||||
|
import ImageBlock from './ImageBlock.vue';
|
||||||
|
import ListBlock from './ListBlock.vue';
|
||||||
|
import QuoteBlock from './QuoteBlock.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
content: {
|
content: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const visibleBlocks = (blocks) => {
|
||||||
|
return blocks.filter((block) => !block.isHidden);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBlockComponent = (type) => {
|
||||||
|
const components = {
|
||||||
|
text: TextBlock,
|
||||||
|
heading: HeadingBlock,
|
||||||
|
image: ImageBlock,
|
||||||
|
list: ListBlock,
|
||||||
|
quote: QuoteBlock,
|
||||||
|
};
|
||||||
|
return components[type] || TextBlock;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ export const useNarrativeStore = defineStore('narrative', () => {
|
||||||
template: 'carte',
|
template: 'carte',
|
||||||
title: child.title,
|
title: child.title,
|
||||||
tags: child.tags,
|
tags: child.tags,
|
||||||
text: child.text,
|
image: child.image,
|
||||||
|
intro: child.intro,
|
||||||
|
markers: child.markers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue