Compare commits

..

No commits in common. "3b59127fa9362c34c50741923f4def9a9bed6dcd" and "ea0994ed45d7d7b6594ef679cbb605fc81b0e1a0" have entirely different histories.

14 changed files with 90 additions and 270 deletions

View file

@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"WebFetch(domain:developers.brevo.com)",
"WebSearch",
"WebFetch(domain:getkirby.com)",
"Bash(mkdir:*)",
"Bash(composer require:*)",
"Bash(composer config:*)",
"Bash(git checkout:*)",
"WebFetch(domain:docs.snipcart.com)",
"WebFetch(domain:www.mondialrelay.fr)",
"WebFetch(domain:storage.mondialrelay.fr)",
"WebFetch(domain:jsfiddle.net)",
"Bash(tree:*)",
"Bash(git add:*)",
"Bash(git commit:*)"
],
"deny": [],
"ask": []
}
}

5
.gitignore vendored
View file

@ -27,6 +27,5 @@ dist-ssr
# Variables d'environnement Brevo
api/.env
# Claude settings
.claude
/.claude/*
# Guide d'intégration (contient des informations sensibles)
GUIDE-INTEGRATION-MONDIAL-RELAY.md

206
README.md
View file

@ -1,205 +1,5 @@
# GeoProject - Web-to-Print Interface
# Vue 3 + Vite
A web-to-print application for creating printable narratives with real-time layout editing.
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Overview
GeoProject is a sophisticated web-to-print platform that combines:
- **Kirby CMS** for content management
- **Vue 3** for interactive editing interface
- **PagedJS** for print-ready rendering
The application allows users to create and edit multi-page narratives with dynamic layouts, supporting various content blocks (text, images, videos, maps) and custom page templates.
## Tech Stack
- **Frontend**: Vue 3 (Composition API) + Vite
- **Print Engine**: PagedJS (CSS Paged Media)
- **CMS**: Kirby 5 (headless, flat-file)
- **Backend**: PHP 8.1+
- **State Management**: Pinia
- **Styling**: CSS with CSS Variables
## Project Structure
```
/src # Vue 3 SPA
├── main.js # Vue bootstrap
├── App.vue # Root component + PagedJS init
├── components/
│ ├── blocks/ # Content block components (HeadingBlock, TextBlock, etc.)
│ ├── editor/ # Editor panels (PageSettings, TextSettings, etc.)
│ ├── ui/ # Reusable UI components (InputWithUnit, MarginEditor, etc.)
│ └── *.vue # Core components (PagedJsWrapper, ElementPopup, etc.)
├── composables/ # Vue composables (useCssSync, useCssUpdater, etc.)
├── stores/ # Pinia stores (narrative.js, stylesheet.js)
└── utils/ # JavaScript utilities
/public # Kirby CMS + static assets
├── site/
│ ├── blueprints/ # Content schemas
│ ├── templates/ # PHP templates
│ ├── snippets/ # PHP snippets
│ └── plugins/ # Kirby plugins
├── content/ # Markdown content files
└── assets/ # Static assets (CSS, fonts, SVG)
/.forgejo/workflows # CI/CD pipeline
```
## Key Features
### Content Types
- **Narratives**: Main story containers with cover, author, introduction
- **Geoformats**: Structured content sections with chapters
- **Chapters**: Individual chapters with rich content blocks
- **Maps**: Special map-based content pages
### Content Blocks
- Text blocks with rich formatting
- Headings with customizable levels
- Images with captions and positioning
- Lists (ordered and unordered)
- Blockquotes with citations
- Video embeds
- Interactive maps
### Print Features
- Real-time preview with PagedJS rendering
- Custom @page rules for different templates
- Interactive element and page editing
- CSS variable-based theming
- Print-optimized output
## Getting Started
### Development
```bash
# Install dependencies
npm install
# Start Vite dev server
npm run dev
# Start PHP server for Kirby (separate terminal)
php -S localhost:8000 -t public
```
The Vue app will be served at `http://localhost:5173` and Kirby at `http://localhost:8000`.
### Production Build
```bash
# Build for production
npm run build
```
Builds are output to `/public/assets/dist/`.
## Data Flow
1. **Kirby CMS** stores and manages content as flat files
2. **PHP Templates** render the HTML structure and inject Vue
3. **Vue App** provides the interactive editing interface
4. **PagedJS** transforms content into print-ready pages
## API
### Narrative JSON Endpoint
```
GET /projet/{narrative-slug}.json
```
Returns the complete narrative data structure including all child pages, blocks, and metadata.
## Naming Conventions
- **Vue Components**: PascalCase (e.g., `PagedJsWrapper.vue`)
- **Composables**: Prefixed with `use` (e.g., `useCssSync`)
- **Stores**: camelCase files, PascalCase store names (e.g., `useNarrativeStore`)
- **Code Language**: English preferred for all code, comments, and identifiers
## English-French Dictionary
The codebase uses English naming conventions, but some French terms remain in content and templates for compatibility. Here's a reference guide:
### Core Concepts
| English | French | Context |
|---------|--------|---------|
| narrative | récit | Main content container type |
| chapter | chapitre | Chapter/section within a geoformat |
| map | carte | Map-based content page |
| cover | couverture | Cover page/image |
| author | auteur | Narrative author(s) |
| introduction | introduction | Introductory text |
| print | impression | Print/output functionality |
### Template Types
| English | French | File/Template Name |
|---------|--------|--------------------|
| narrative | recit | `narrative.php`, `narrative.json.php` |
| chapter | chapitre | `chapitre.php` |
| map | carte | `carte.php` |
| geoformat | geoformat | `geoformat.php` |
### UI Elements
| English | French | Notes |
|---------|--------|-------|
| settings | paramètres | Editor panel settings |
| page | page | Page template/type |
| block | bloc | Content block |
| edit | éditer | Edit action |
| preview | aperçu | Preview mode |
### Technical Terms
| English | French | Notes |
|---------|--------|-------|
| store | magasin | Pinia store (use English 'store') |
| template | template/modèle | Page template |
| blueprint | schéma | Kirby content schema |
| field | champ | Form/content field |
### Code Examples
**Store naming:**
```javascript
// Correct
import { useNarrativeStore } from './stores/narrative';
// Old (deprecated)
import { useRecitStore } from './stores/recit';
```
**Template references:**
```javascript
// Check for narrative template
if (item.template === 'narrative') { /* ... */ }
// Check for chapter template
if (item.template === 'chapitre') { /* ... */ }
```
**CSS classes:**
```css
/* Narrative cover page */
.narrative-cover { /* ... */ }
/* Chapter content */
.chapitre { /* ... */ }
```
## CI/CD
The project uses Forgejo Actions for continuous deployment:
1. Code is pushed to Forgejo repository
2. Workflow builds the Vue app
3. Files are deployed via FTP to production server
See `.forgejo/workflows/deploy.yml` for details.
## Contributing
For detailed development guidelines, see `CLAUDE.md`.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

View file

@ -2,7 +2,7 @@
## Vue d'ensemble
Application web-to-print permettant la mise en page de récits (narratives) imprimables. L'édition de contenu se fait via Kirby CMS (headless), la mise en page est rendue par PagedJS, et l'interface d'édition réactive utilise Vue 3.
Application web-to-print permettant la mise en page de récits imprimables. L'édition de contenu se fait via Kirby CMS (headless), la mise en page est rendue par PagedJS, et l'interface d'édition réactive utilise Vue 3.
## Stack technique
@ -30,7 +30,7 @@ Application web-to-print permettant la mise en page de récits (narratives) impr
│ ├── PreviewLoader.vue # Loader de preview
│ └── StylesheetViewer.vue # Viewer de feuilles de style
├── composables/ # COMPOSABLES VUE (useCssSync, useCssUpdater, etc.)
├── stores/ # STORES PINIA (narrative.js, stylesheet.js)
├── stores/ # STORES PINIA (recit.js, stylesheet.js)
└── utils/ # UTILITAIRES JS (css-parsing.js, etc.)
/api/cache # Cache des données API (donorbox_data.json, etc.)
@ -78,7 +78,7 @@ Application web-to-print permettant la mise en page de récits (narratives) impr
### State Management
- **Pinia** utilisé pour la gestion d'état
- `stores/narrative.js` : État du récit/narrative (contenu, navigation)
- `stores/recit.js` : État du récit (contenu, navigation)
- `stores/stylesheet.js` : État des feuilles de style CSS
### Conventions de placement (IMPORTANT : respecter cette organisation)
@ -149,6 +149,5 @@ site/sessions/ # Sessions actives
- Composants Vue : PascalCase
- CSS : Variables pour theming, scoped styles
- Print CSS : W3C Paged Media spec
- Stores Pinia : camelCase pour les fichiers, PascalCase pour les noms (ex: `useNarrativeStore`)
- Stores Pinia : camelCase pour les fichiers, PascalCase pour les noms (ex: `useRecitStore`)
- Composables : Préfixe `use` (ex: `useCssSync`)
- Code naming : English preferred (ex: `narrative` instead of `recit`)

View file

@ -17,9 +17,9 @@ columns:
multiple: false
width: 1/2
pages:
label: Narratives
label: Récits
type: pages
template: narrative
template: recit
sidebar:
width: 1/3
sections:

View file

@ -1,4 +1,4 @@
title: Narrative
title: Récit
columns:
main:

View file

@ -2,8 +2,8 @@
/**
* Virtual Print Page Plugin
*
* Creates a virtual /print page for each narrative
* Allows access to print editor via /projet/narrative/print
* Crée une page virtuelle /print pour chaque récit
* Permet d'accéder à l'éditeur d'impression via /projet/recit/print
*/
use Kirby\Cms\Page;
@ -14,20 +14,20 @@ Kirby::plugin('geoproject/virtual-print-page', [
[
'pattern' => '(:all)/print',
'action' => function ($parentPath) {
// Find parent page (the narrative)
// Trouver la page parente (le récit)
$parent = page($parentPath);
if (!$parent || $parent->intendedTemplate()->name() !== 'narrative') {
if (!$parent || $parent->intendedTemplate()->name() !== 'recit') {
return $this->next();
}
// Create virtual page with Page::factory()
// Créer la page virtuelle avec Page::factory()
return Page::factory([
'slug' => 'print',
'template' => 'print',
'parent' => $parent,
'content' => [
'title' => 'Print - ' . $parent->title()->value(),
'title' => 'Impression - ' . $parent->title()->value(),
'uuid' => Uuid::generate()
]
]);

View file

@ -26,5 +26,5 @@
<?php endif ?>
</head>
<body data-template="<?= $page->template() ?>"<?php if (isset($narrativeJsonUrl)): ?> data-narrative-url="<?= $narrativeJsonUrl ?>"<?php endif ?>>
<body data-template="<?= $page->template() ?>"<?php if (isset($recitJsonUrl)): ?> data-recit-url="<?= $recitJsonUrl ?>"<?php endif ?>>
<div id="app">

View file

@ -1,17 +1,17 @@
<?php
/**
* Template for Vue.js print editor
* Route: /projet/narrative/print
* Template pour l'éditeur d'impression Vue.js
* Route: /projet/recit/print
*
* This template loads the Vue app and passes the parent narrative JSON URL
* Ce template charge l'app Vue et lui passe l'URL JSON du récit parent
*/
// Get parent narrative
$narrative = $page->parent();
// Récupérer le récit parent
$recit = $page->parent();
// Build narrative JSON URL
$narrativeJsonUrl = $narrative->url() . '.json';
// Construire l'URL JSON du récit
$recitJsonUrl = $recit->url() . '.json';
?>
<?php snippet('header', ['narrativeJsonUrl' => $narrativeJsonUrl]) ?>
<?php snippet('header', ['recitJsonUrl' => $recitJsonUrl]) ?>
<?php snippet('footer') ?>

View file

@ -1,7 +1,7 @@
<?php
/**
* JSON template to expose narrative data
* Accessible via /projet/narrative.json or /projet/narrative?format=json
* Template JSON pour exposer les données d'un récit
* Accessible via /projet/recit.json ou /projet/recit?format=json
*/
header('Content-Type: application/json; charset=utf-8');
@ -175,11 +175,11 @@ function parseGeoformat($geoformat) {
];
}
// Build JSON response
// Construction de la réponse JSON
$data = [
'id' => $page->id(),
'uuid' => $page->uuid()->toString(),
'template' => 'narrative',
'template' => 'recit',
'title' => $page->title()->value(),
'slug' => $page->slug(),
'author' => $page->author()->value() ?? '',
@ -188,7 +188,7 @@ $data = [
'children' => []
];
// Parse children (geoformats and maps)
// Parser les enfants (geoformats et cartes)
foreach ($page->children()->listed() as $child) {
$template = $child->intendedTemplate()->name();

View file

@ -1,12 +1,12 @@
<?php
/**
* Template to display a narrative
* This template is required for narrative.json.php to work
* Template pour afficher un récit
* Ce template est requis pour que recit.json.php fonctionne
*/
?>
<?php snippet('header') ?>
<article class="narrative">
<article class="recit">
<h1><?= $page->title() ?></h1>
<?php if ($page->author()->isNotEmpty()): ?>
@ -27,7 +27,7 @@
</div>
<?php endif ?>
<p><a href="<?= $page->url() ?>/print">Open print editor</a></p>
<p><a href="<?= $page->url() ?>/print">Ouvrir l'éditeur d'impression</a></p>
</article>
<?php snippet('footer') ?>

View file

@ -6,14 +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 { useNarrativeStore } from './stores/narrative';
import { useRecitStore } from './stores/recit';
import Coloris from '@melloware/coloris';
const stylesheetStore = useStylesheetStore();
const narrativeStore = useNarrativeStore();
const recitStore = useRecitStore();
// Get narrative URL from body data attribute (set by print.php template)
const narrativeUrl = document.body.dataset.narrativeUrl || null;
// 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);
@ -488,11 +488,11 @@ watch(
}
);
// Re-render when narrative data changes
// Re-render when recit data changes
watch(
() => narrativeStore.data,
() => recitStore.data,
() => {
if (narrativeStore.data) {
if (recitStore.data) {
renderPreview();
}
}
@ -558,9 +558,9 @@ const printPreview = async () => {
};
onMounted(async () => {
// Load narrative data if URL is provided (print mode)
if (narrativeUrl) {
await narrativeStore.loadNarrative(narrativeUrl);
// Load recit data if URL is provided (print mode)
if (recitUrl) {
await recitStore.loadRecit(recitUrl);
}
// Render preview after data is loaded

View file

@ -1,6 +1,6 @@
<template>
<!-- Fallback static content when no narrative data -->
<template v-if="!hasNarrativeData">
<!-- Fallback static content when no recit data -->
<template v-if="!hasRecitData">
<section class="chapter">
<p>
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit
@ -10,13 +10,13 @@
</section>
</template>
<!-- Dynamic content from narrative -->
<!-- Dynamic content from recit -->
<template v-else>
<template v-for="item in flattenedContent" :key="item.id">
<!-- Narrative (cover page) -->
<!-- Récit (cover page) -->
<section
v-if="item.template === 'narrative'"
class="narrative-cover"
v-if="item.template === 'recit'"
class="recit-cover"
:data-page-type="item.template"
>
<img v-if="item.cover" :src="item.cover" class="cover-image" alt="" />
@ -75,7 +75,7 @@
<script setup>
import { computed } from 'vue';
import { useNarrativeStore } from '../stores/narrative';
import { useRecitStore } from '../stores/recit';
import {
TextBlock,
HeadingBlock,
@ -87,10 +87,10 @@ import {
blockComponents
} from './blocks';
const narrativeStore = useNarrativeStore();
const recitStore = useRecitStore();
const hasNarrativeData = computed(() => narrativeStore.data !== null);
const flattenedContent = computed(() => narrativeStore.flattenedContent);
const hasRecitData = computed(() => recitStore.data !== null);
const flattenedContent = computed(() => recitStore.flattenedContent);
// Filter out hidden blocks
const visibleBlocks = (blocks) => {
@ -114,24 +114,24 @@ const getBlockComponent = (type) => {
<style>
/* Base print styles for content sections */
.narrative-cover,
.recit-cover,
.geoformat,
.chapitre,
.carte {
break-before: page;
}
.narrative-cover .cover-image,
.recit-cover .cover-image,
.geoformat .cover-image {
max-width: 100%;
height: auto;
}
.narrative-cover h1 {
.recit-cover h1 {
margin-top: 1rem;
}
.narrative-cover .author {
.recit-cover .author {
font-style: italic;
color: #666;
}

View file

@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useNarrativeStore = defineStore('narrative', () => {
export const useRecitStore = defineStore('recit', () => {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
@ -19,10 +19,10 @@ export const useNarrativeStore = defineStore('narrative', () => {
const items = [];
// Add narrative intro as first section
// Add recit intro as first section
items.push({
id: data.value.id,
template: 'narrative',
template: 'recit',
title: data.value.title,
author: data.value.author,
cover: data.value.cover,
@ -72,10 +72,10 @@ export const useNarrativeStore = defineStore('narrative', () => {
return items;
});
// Load narrative data from URL
const loadNarrative = async (url) => {
// Load recit data from URL
const loadRecit = async (url) => {
if (!url) {
error.value = 'No narrative URL provided';
error.value = 'No recit URL provided';
return;
}
@ -91,7 +91,7 @@ export const useNarrativeStore = defineStore('narrative', () => {
data.value = await response.json();
} catch (e) {
console.error('Error loading narrative:', e);
console.error('Error loading recit:', e);
error.value = e.message;
data.value = null;
} finally {
@ -121,7 +121,7 @@ export const useNarrativeStore = defineStore('narrative', () => {
flattenedContent,
// Actions
loadNarrative,
loadRecit,
reset
};
});