From 790eb7414ed246def230091b04379075412ebe8f Mon Sep 17 00:00:00 2001 From: isUnknown Date: Mon, 8 Dec 2025 18:01:01 +0100 Subject: [PATCH] feat: integrate Kirby CMS data with Vue print editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add JSON content representation template (recit.json.php) - Create virtual /print page plugin for recit pages - Add recit.php base template for content representation - Create Pinia store for recit data management - Add block components (text, heading, image, list, quote, video, map) - Update PagedJsWrapper for dynamic content rendering with data-page-type - Modify header.php to pass recit JSON URL via data attribute - Update App.vue to load recit data on mount 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/site/blueprints/pages/recit.yml | 14 +- .../site/plugins/virtual-print-page/index.php | 37 +++ public/site/snippets/header.php | 2 +- public/site/templates/print.php | 17 ++ public/site/templates/recit.json.php | 202 +++++++++++++++ public/site/templates/recit.php | 33 +++ src/App.vue | 25 +- src/components/PagedJsWrapper.vue | 234 ++++++++++++++---- src/components/blocks/HeadingBlock.vue | 23 ++ src/components/blocks/ImageBlock.vue | 40 +++ src/components/blocks/ListBlock.vue | 12 + src/components/blocks/MapBlock.vue | 14 ++ src/components/blocks/QuoteBlock.vue | 15 ++ src/components/blocks/TextBlock.vue | 12 + src/components/blocks/VideoBlock.vue | 38 +++ src/components/blocks/index.js | 18 ++ src/stores/recit.js | 127 ++++++++++ 17 files changed, 807 insertions(+), 56 deletions(-) create mode 100644 public/site/plugins/virtual-print-page/index.php create mode 100644 public/site/templates/print.php create mode 100644 public/site/templates/recit.json.php create mode 100644 public/site/templates/recit.php create mode 100644 src/components/blocks/HeadingBlock.vue create mode 100644 src/components/blocks/ImageBlock.vue create mode 100644 src/components/blocks/ListBlock.vue create mode 100644 src/components/blocks/MapBlock.vue create mode 100644 src/components/blocks/QuoteBlock.vue create mode 100644 src/components/blocks/TextBlock.vue create mode 100644 src/components/blocks/VideoBlock.vue create mode 100644 src/components/blocks/index.js create mode 100644 src/stores/recit.js diff --git a/public/site/blueprints/pages/recit.yml b/public/site/blueprints/pages/recit.yml index 92a7489..7640015 100644 --- a/public/site/blueprints/pages/recit.yml +++ b/public/site/blueprints/pages/recit.yml @@ -7,22 +7,25 @@ columns: fields: type: fields fields: - author: + blueprint: + type: headline + label: Page "{{ page.intendedTemplate }}" + author: label: Auteur·ice(s) type: text width: 1/2 - cover: + cover: label: Image de couverture type: files multiple: false width: 1/2 - introduction: + introduction: label: Introduction type: writer pages: label: Pages type: pages - template: + template: - carte - geoformat sidebar: @@ -31,6 +34,3 @@ columns: files: label: Fichiers type: files - - - diff --git a/public/site/plugins/virtual-print-page/index.php b/public/site/plugins/virtual-print-page/index.php new file mode 100644 index 0000000..0e4bce9 --- /dev/null +++ b/public/site/plugins/virtual-print-page/index.php @@ -0,0 +1,37 @@ + [ + [ + 'pattern' => '(:all)/print', + 'action' => function ($parentPath) { + // Trouver la page parente (le récit) + $parent = page($parentPath); + + if (!$parent || $parent->intendedTemplate()->name() !== 'recit') { + return $this->next(); + } + + // Créer la page virtuelle avec Page::factory() + return Page::factory([ + 'slug' => 'print', + 'template' => 'print', + 'parent' => $parent, + 'content' => [ + 'title' => 'Impression - ' . $parent->title()->value(), + 'uuid' => Uuid::generate() + ] + ]); + } + ] + ] +]); diff --git a/public/site/snippets/header.php b/public/site/snippets/header.php index 75fd430..e420f16 100644 --- a/public/site/snippets/header.php +++ b/public/site/snippets/header.php @@ -26,5 +26,5 @@ - + data-recit-url="">
\ No newline at end of file diff --git a/public/site/templates/print.php b/public/site/templates/print.php new file mode 100644 index 0000000..8546ada --- /dev/null +++ b/public/site/templates/print.php @@ -0,0 +1,17 @@ +parent(); + +// Construire l'URL JSON du récit +$recitJsonUrl = $recit->url() . '.json'; +?> + $recitJsonUrl]) ?> + + diff --git a/public/site/templates/recit.json.php b/public/site/templates/recit.json.php new file mode 100644 index 0000000..6c4ad10 --- /dev/null +++ b/public/site/templates/recit.json.php @@ -0,0 +1,202 @@ +isEmpty()) { + return null; + } + + $file = $field->toFile(); + if ($file) { + return $file->url(); + } + + return null; +} + +/** + * Résout les images dans le contenu HTML + */ +function resolveImagesInHtml($html, $page) { + if (!$html) return $html; + + // Remplacer les références file:// par les URLs réelles + return preg_replace_callback( + '/file:\/\/([a-z0-9]+)/i', + function($matches) use ($page) { + $uuid = $matches[1]; + $file = $page->file("file://{$uuid}"); + if (!$file) { + // Chercher dans les fichiers du site + $file = site()->file("file://{$uuid}"); + } + return $file ? $file->url() : $matches[0]; + }, + $html + ); +} + +/** + * Parse les blocks Kirby en structure JSON + */ +function parseBlocks($blocksField, $page) { + if (!$blocksField || $blocksField->isEmpty()) { + return []; + } + + $blocks = []; + foreach ($blocksField->toBlocks() as $block) { + $blockData = [ + 'id' => $block->id(), + 'type' => $block->type(), + 'isHidden' => $block->isHidden(), + ]; + + switch ($block->type()) { + case 'text': + $blockData['content'] = [ + 'text' => resolveImagesInHtml($block->text()->value(), $page) + ]; + break; + + case 'heading': + $blockData['content'] = [ + 'level' => $block->level()->value() ?? 'h2', + 'text' => $block->text()->value() + ]; + break; + + case 'image': + $image = $block->image()->toFile(); + $blockData['content'] = [ + 'url' => $image ? $image->url() : null, + 'alt' => $block->alt()->value() ?? '', + 'caption' => $block->caption()->value() ?? '', + 'width' => $block->width()->value() ?? '100%', + 'position' => $block->position()->value() ?? 'auto' + ]; + break; + + case 'list': + $blockData['content'] = [ + 'text' => $block->text()->value() + ]; + break; + + case 'quote': + $blockData['content'] = [ + 'text' => $block->text()->value(), + 'citation' => $block->citation()->value() ?? '' + ]; + break; + + case 'video': + $blockData['content'] = [ + 'url' => $block->url()->value(), + 'caption' => $block->caption()->value() ?? '' + ]; + break; + + case 'map': + $blockData['content'] = [ + 'map' => $block->map()->value() + ]; + break; + + default: + $blockData['content'] = $block->content()->toArray(); + } + + $blocks[] = $blockData; + } + + return $blocks; +} + +/** + * Parse un chapitre + */ +function parseChapitre($chapitre) { + return [ + 'id' => $chapitre->id(), + 'uuid' => $chapitre->uuid()->toString(), + 'template' => 'chapitre', + 'title' => $chapitre->title()->value(), + 'slug' => $chapitre->slug(), + 'blocks' => parseBlocks($chapitre->text(), $chapitre) + ]; +} + +/** + * Parse une carte + */ +function parseCarte($carte) { + return [ + 'id' => $carte->id(), + 'uuid' => $carte->uuid()->toString(), + 'template' => 'carte', + 'title' => $carte->title()->value(), + 'slug' => $carte->slug(), + 'tags' => $carte->tags()->isNotEmpty() ? $carte->tags()->split() : [], + 'text' => resolveImagesInHtml($carte->text()->value(), $carte) + ]; +} + +/** + * Parse un geoformat + */ +function parseGeoformat($geoformat) { + $chapitres = []; + foreach ($geoformat->children()->listed() as $child) { + if ($child->intendedTemplate()->name() === 'chapitre') { + $chapitres[] = parseChapitre($child); + } + } + + return [ + 'id' => $geoformat->id(), + 'uuid' => $geoformat->uuid()->toString(), + 'template' => 'geoformat', + 'title' => $geoformat->title()->value(), + 'slug' => $geoformat->slug(), + 'subtitle' => $geoformat->subtitle()->value() ?? '', + 'tags' => $geoformat->tags()->isNotEmpty() ? $geoformat->tags()->split() : [], + 'cover' => resolveFileUrl($geoformat->cover(), $geoformat), + 'text' => resolveImagesInHtml($geoformat->text()->value(), $geoformat), + 'children' => $chapitres + ]; +} + +// Construction de la réponse JSON +$data = [ + 'id' => $page->id(), + 'uuid' => $page->uuid()->toString(), + 'template' => 'recit', + 'title' => $page->title()->value(), + 'slug' => $page->slug(), + 'author' => $page->author()->value() ?? '', + 'cover' => resolveFileUrl($page->cover(), $page), + 'introduction' => resolveImagesInHtml($page->introduction()->value(), $page), + 'children' => [] +]; + +// Parser les enfants (geoformats et cartes) +foreach ($page->children()->listed() as $child) { + $template = $child->intendedTemplate()->name(); + + if ($template === 'geoformat') { + $data['children'][] = parseGeoformat($child); + } elseif ($template === 'carte') { + $data['children'][] = parseCarte($child); + } +} + +echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/public/site/templates/recit.php b/public/site/templates/recit.php new file mode 100644 index 0000000..e0d016f --- /dev/null +++ b/public/site/templates/recit.php @@ -0,0 +1,33 @@ + + + +
+

title() ?>

+ + author()->isNotEmpty()): ?> +

author() ?>

+ + + cover()->isNotEmpty()): ?> +
+ cover()->toFile()): ?> + + +
+ + + introduction()->isNotEmpty()): ?> +
+ introduction() ?> +
+ + +

Ouvrir l'éditeur d'impression

+
+ + diff --git a/src/App.vue b/src/App.vue index 5d129f6..d3b69f8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,9 +6,14 @@ import PagePopup from './components/PagePopup.vue'; import PreviewLoader from './components/PreviewLoader.vue'; import { onMounted, ref, watch, computed, provide } from 'vue'; import { useStylesheetStore } from './stores/stylesheet'; +import { useRecitStore } from './stores/recit'; import Coloris from '@melloware/coloris'; const stylesheetStore = useStylesheetStore(); +const recitStore = useRecitStore(); + +// Get recit URL from body data attribute (set by print.php template) +const recitUrl = document.body.dataset.recitUrl || null; const previewFrame1 = ref(null); const previewFrame2 = ref(null); const elementPopup = ref(null); @@ -500,6 +505,16 @@ watch( } ); +// Re-render when recit data changes +watch( + () => recitStore.data, + () => { + if (recitStore.data) { + renderPreview(); + } + } +); + // Print the PagedJS content const printPreview = async () => { const frame = activeFrame.value; @@ -559,7 +574,15 @@ const printPreview = async () => { }, 100); }; -onMounted(() => renderPreview(true)); +onMounted(async () => { + // Load recit data if URL is provided (print mode) + if (recitUrl) { + await recitStore.loadRecit(recitUrl); + } + + // Render preview after data is loaded + renderPreview(true); +});