All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 25s
250 lines
5.8 KiB
Vue
250 lines
5.8 KiB
Vue
<template>
|
|
<!-- Fallback static content when no narrative data -->
|
|
<template v-if="!hasNarrativeData">
|
|
<section class="chapter">
|
|
<p>
|
|
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit
|
|
phasellus euismod maximus rutrum vivamus dolor erat sollicitudin ut quam
|
|
metus gravida proin nisl lacus sed lacus.
|
|
</p>
|
|
</section>
|
|
</template>
|
|
|
|
<!-- Dynamic content from narrative -->
|
|
<template v-else>
|
|
<template v-for="item in flattenedContent" :key="item.id">
|
|
<!-- Narrative (cover page) -->
|
|
<section
|
|
v-if="item.template === 'narrative'"
|
|
class="narrative-cover"
|
|
:data-page-type="item.template"
|
|
>
|
|
<figure class="figure-backgroung-cover">
|
|
<img v-if="item.cover" :src="item.cover" class="cover-image" alt="" />
|
|
</figure>
|
|
<h1>{{ item.title }}</h1>
|
|
<p v-if="item.author" class="author">{{ item.author }}</p>
|
|
<div v-if="item.introduction" class="introduction" v-html="item.introduction"></div>
|
|
</section>
|
|
|
|
<!-- Geoformat -->
|
|
<section
|
|
v-else-if="item.template === 'geoformat'"
|
|
class="geoformat"
|
|
:data-page-type="item.template"
|
|
>
|
|
<img v-if="item.cover" :src="item.cover" class="cover-image" alt="" />
|
|
<h2>{{ item.title }}</h2>
|
|
<p v-if="item.subtitle" class="subtitle">{{ item.subtitle }}</p>
|
|
<div v-if="item.tags && item.tags.length" class="tags">
|
|
<span v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</span>
|
|
</div>
|
|
<div v-if="item.text" class="chapeau" v-html="item.text"></div>
|
|
</section>
|
|
|
|
<!-- Chapter -->
|
|
<section
|
|
v-else-if="item.template === 'chapter'"
|
|
class="chapter"
|
|
:data-page-type="item.template"
|
|
>
|
|
<h3>{{ item.title }}</h3>
|
|
<template v-if="item.blocks">
|
|
<component
|
|
v-for="block in visibleBlocks(item.blocks)"
|
|
:key="block.id"
|
|
:is="getBlockComponent(block.type)"
|
|
:content="block.content"
|
|
/>
|
|
</template>
|
|
</section>
|
|
|
|
<!-- Carte -->
|
|
<section
|
|
v-else-if="item.template === 'carte'"
|
|
class="carte"
|
|
:data-page-type="item.template"
|
|
>
|
|
<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">
|
|
<span v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</span>
|
|
</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"
|
|
alt=""
|
|
/>
|
|
<img
|
|
v-else
|
|
src="/assets/svg/marker-pin.svg"
|
|
class="marker-icon"
|
|
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>
|
|
</template>
|
|
</template>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue';
|
|
import { useNarrativeStore } from '../stores/narrative';
|
|
import {
|
|
TextBlock,
|
|
HeadingBlock,
|
|
ImageBlock,
|
|
ListBlock,
|
|
QuoteBlock,
|
|
VideoBlock,
|
|
MapBlock,
|
|
blockComponents
|
|
} from './blocks';
|
|
|
|
const narrativeStore = useNarrativeStore();
|
|
|
|
const hasNarrativeData = computed(() => narrativeStore.data !== null);
|
|
const flattenedContent = computed(() => narrativeStore.flattenedContent);
|
|
|
|
// Filter out hidden blocks
|
|
const visibleBlocks = (blocks) => {
|
|
return blocks.filter((block) => !block.isHidden);
|
|
};
|
|
|
|
// Get the component for a block type
|
|
const getBlockComponent = (type) => {
|
|
const components = {
|
|
text: TextBlock,
|
|
heading: HeadingBlock,
|
|
image: ImageBlock,
|
|
list: ListBlock,
|
|
quote: QuoteBlock,
|
|
video: VideoBlock,
|
|
map: MapBlock
|
|
};
|
|
return components[type] || TextBlock;
|
|
};
|
|
</script>
|
|
|
|
<style>
|
|
/* Base print styles for content sections */
|
|
.narrative-cover,
|
|
.geoformat,
|
|
.chapter,
|
|
.carte {
|
|
break-before: page;
|
|
}
|
|
|
|
.narrative-cover .cover-image,
|
|
.geoformat .cover-image {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.narrative-cover h1 {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.narrative-cover .author {
|
|
font-style: italic;
|
|
color: #666;
|
|
}
|
|
|
|
.tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.tag {
|
|
background: #f0f0f0;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Block styles */
|
|
.block-image img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.block-quote {
|
|
border-left: 3px solid #ccc;
|
|
padding-left: 1rem;
|
|
margin-left: 0;
|
|
font-style: italic;
|
|
}
|
|
|
|
.block-quote cite {
|
|
display: block;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: #666;
|
|
}
|
|
|
|
.block-video .video-embed {
|
|
position: relative;
|
|
padding-bottom: 56.25%;
|
|
height: 0;
|
|
}
|
|
|
|
.block-video .video-embed iframe,
|
|
.block-video .video-embed video {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.block-map {
|
|
break-before: page;
|
|
}
|
|
|
|
.carte-image {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.marker {
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.marker-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.marker-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
flex-shrink: 0;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.marker-cover {
|
|
max-width: 100%;
|
|
height: auto;
|
|
margin: 0.5rem 0;
|
|
}
|
|
</style>
|