Initial commit
This commit is contained in:
174
README.md
Normal file
174
README.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# EU Media Guide CMS
|
||||||
|
|
||||||
|
CMS basato su Grav per la gestione dei contenuti multilingua della media guide.
|
||||||
|
|
||||||
|
## Struttura del Progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
grav-cms-mediaguide/
|
||||||
|
├── docker-compose.yml # Config Docker per Coolify
|
||||||
|
├── config/
|
||||||
|
│ ├── system.yaml # Config sistema (24 lingue EU)
|
||||||
|
│ └── site.yaml # Config sito
|
||||||
|
├── blueprints/
|
||||||
|
│ └── station.yaml # Blueprint per le stazioni/moduli
|
||||||
|
├── pages/
|
||||||
|
│ └── stations/
|
||||||
|
│ └── test-module/ # Modulo di test
|
||||||
|
├── plugins/
|
||||||
|
│ └── json-export/ # Plugin per export JSON + API
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy su Coolify
|
||||||
|
|
||||||
|
### 1. Crea Repository Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd grav-cms-mediaguide
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial CMS setup"
|
||||||
|
git remote add origin <your-repo-url>
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy su Coolify
|
||||||
|
|
||||||
|
1. In Coolify, crea un nuovo servizio **Docker Compose**
|
||||||
|
2. Collega il repository Git
|
||||||
|
3. Imposta la porta pubblica su `8080`
|
||||||
|
4. Deploy!
|
||||||
|
|
||||||
|
### 3. Setup Iniziale Grav
|
||||||
|
|
||||||
|
Dopo il primo deploy, accedi al container e crea l'utente admin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Accedi al container
|
||||||
|
docker exec -it mediaguide-cms bash
|
||||||
|
|
||||||
|
# Crea utente admin
|
||||||
|
bin/plugin login new-user
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Installa Plugin Admin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/gpm install admin
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Il plugin `json-export` espone questi endpoint:
|
||||||
|
|
||||||
|
| Endpoint | Descrizione |
|
||||||
|
|----------|-------------|
|
||||||
|
| `GET /api/stations?lang=en` | Lista tutte le stazioni |
|
||||||
|
| `GET /api/station/{id}?lang=en` | Contenuto singola stazione |
|
||||||
|
| `GET /api/translations?lang=en` | Traduzioni UI |
|
||||||
|
| `GET /api/export` | Esporta tutto in file JSON |
|
||||||
|
|
||||||
|
### Esempio Risposta `/api/station/test-welcome?lang=en`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "test-welcome",
|
||||||
|
"type": "intro",
|
||||||
|
"language": "en",
|
||||||
|
"title": "Welcome to the European Parliament",
|
||||||
|
"description": "Your interactive guide to European democracy",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "welcome-heading",
|
||||||
|
"type": "heading",
|
||||||
|
"level": "h1",
|
||||||
|
"content": "Welcome to the European Parliament"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "intro-text",
|
||||||
|
"type": "text",
|
||||||
|
"content": "<p>Welcome to the heart of European democracy...</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "welcome-video",
|
||||||
|
"type": "video",
|
||||||
|
"src": "/media/videos/welcome-en.mp4",
|
||||||
|
"poster": "/media/images/video-poster.jpg",
|
||||||
|
"caption": "A brief introduction to the European Parliament"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lingue Supportate (Ordine Protocollo UE)
|
||||||
|
|
||||||
|
| Codice | Lingua |
|
||||||
|
|--------|--------|
|
||||||
|
| bg | български |
|
||||||
|
| es | español |
|
||||||
|
| cs | čeština |
|
||||||
|
| da | dansk |
|
||||||
|
| de | Deutsch |
|
||||||
|
| et | eesti |
|
||||||
|
| el | ελληνικά |
|
||||||
|
| en | English |
|
||||||
|
| fr | français |
|
||||||
|
| ga | Gaeilge |
|
||||||
|
| hr | hrvatski |
|
||||||
|
| it | italiano |
|
||||||
|
| lv | latviešu |
|
||||||
|
| lt | lietuvių |
|
||||||
|
| hu | magyar |
|
||||||
|
| mt | Malti |
|
||||||
|
| nl | Nederlands |
|
||||||
|
| pl | polski |
|
||||||
|
| pt | português |
|
||||||
|
| ro | română |
|
||||||
|
| sk | slovenčina |
|
||||||
|
| sl | slovenščina |
|
||||||
|
| fi | suomi |
|
||||||
|
| sv | svenska |
|
||||||
|
|
||||||
|
## Gestione Contenuti
|
||||||
|
|
||||||
|
### Creare una Nuova Stazione
|
||||||
|
|
||||||
|
1. Accedi all'admin panel: `http://your-domain:8080/admin`
|
||||||
|
2. Vai su **Pages** → **Add** → **Station**
|
||||||
|
3. Compila i campi multilingua
|
||||||
|
4. Aggiungi i componenti (heading, text, video, audio, tabs, slideshow)
|
||||||
|
5. Salva
|
||||||
|
|
||||||
|
### Struttura File Markdown
|
||||||
|
|
||||||
|
Ogni stazione è un file `.md` con frontmatter YAML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: Station Title
|
||||||
|
station_id: unique-id
|
||||||
|
station_type: exhibit
|
||||||
|
active: true
|
||||||
|
order: 1
|
||||||
|
|
||||||
|
title_translations:
|
||||||
|
en: English Title
|
||||||
|
fr: Titre français
|
||||||
|
|
||||||
|
components:
|
||||||
|
- type: heading
|
||||||
|
id: heading-1
|
||||||
|
level: h1
|
||||||
|
content:
|
||||||
|
en: English Heading
|
||||||
|
fr: Titre français
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prossimi Passi
|
||||||
|
|
||||||
|
1. [ ] Deploy del CMS su Coolify
|
||||||
|
2. [ ] Configurare l'admin panel
|
||||||
|
3. [ ] Creare il container React per l'app
|
||||||
|
4. [ ] Collegare l'app agli endpoint API del CMS
|
||||||
181
blueprints/station.yaml
Normal file
181
blueprints/station.yaml
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Station Blueprint
|
||||||
|
# Defines a content module/station for the media guide
|
||||||
|
# /user/blueprints/station.yaml
|
||||||
|
|
||||||
|
title: Station
|
||||||
|
'@extends':
|
||||||
|
type: default
|
||||||
|
context: blueprints://pages
|
||||||
|
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
tabs:
|
||||||
|
type: tabs
|
||||||
|
active: 1
|
||||||
|
|
||||||
|
fields:
|
||||||
|
# Station Info Tab
|
||||||
|
station_info:
|
||||||
|
type: tab
|
||||||
|
title: Station Info
|
||||||
|
fields:
|
||||||
|
header.station_id:
|
||||||
|
type: text
|
||||||
|
label: Station ID
|
||||||
|
help: Unique identifier for this station (used in QR codes)
|
||||||
|
validate:
|
||||||
|
required: true
|
||||||
|
pattern: '^[a-z0-9-]+$'
|
||||||
|
|
||||||
|
header.station_type:
|
||||||
|
type: select
|
||||||
|
label: Station Type
|
||||||
|
options:
|
||||||
|
intro: Introduction
|
||||||
|
exhibit: Exhibit
|
||||||
|
information: Information Point
|
||||||
|
navigation: Navigation
|
||||||
|
default: exhibit
|
||||||
|
|
||||||
|
header.exhibition_ref:
|
||||||
|
type: text
|
||||||
|
label: Exhibition Reference
|
||||||
|
help: Reference to parent exhibition
|
||||||
|
|
||||||
|
header.active:
|
||||||
|
type: toggle
|
||||||
|
label: Active
|
||||||
|
highlight: 1
|
||||||
|
default: 1
|
||||||
|
options:
|
||||||
|
1: Yes
|
||||||
|
0: No
|
||||||
|
|
||||||
|
header.order:
|
||||||
|
type: number
|
||||||
|
label: Display Order
|
||||||
|
default: 0
|
||||||
|
|
||||||
|
# Multilingual Content Tab
|
||||||
|
content_tab:
|
||||||
|
type: tab
|
||||||
|
title: Content
|
||||||
|
fields:
|
||||||
|
header.title:
|
||||||
|
type: multilang
|
||||||
|
label: Title
|
||||||
|
languages: true
|
||||||
|
|
||||||
|
header.description:
|
||||||
|
type: multilang
|
||||||
|
label: Short Description
|
||||||
|
type: textarea
|
||||||
|
languages: true
|
||||||
|
|
||||||
|
# Components Tab
|
||||||
|
components_tab:
|
||||||
|
type: tab
|
||||||
|
title: Components
|
||||||
|
fields:
|
||||||
|
header.components:
|
||||||
|
type: list
|
||||||
|
label: Content Components
|
||||||
|
style: vertical
|
||||||
|
btnLabel: Add Component
|
||||||
|
collapsed: true
|
||||||
|
|
||||||
|
fields:
|
||||||
|
.type:
|
||||||
|
type: select
|
||||||
|
label: Component Type
|
||||||
|
options:
|
||||||
|
heading: Heading
|
||||||
|
text: Text Block
|
||||||
|
video: Video Player
|
||||||
|
audio: Audio Player
|
||||||
|
tabs: Tabbed Content
|
||||||
|
slideshow: Slideshow
|
||||||
|
|
||||||
|
.id:
|
||||||
|
type: text
|
||||||
|
label: Component ID
|
||||||
|
help: Unique ID for this component
|
||||||
|
|
||||||
|
# Heading specific
|
||||||
|
.level:
|
||||||
|
type: select
|
||||||
|
label: Heading Level
|
||||||
|
options:
|
||||||
|
h1: H1
|
||||||
|
h2: H2
|
||||||
|
h3: H3
|
||||||
|
|
||||||
|
# Text/Heading content (multilingual)
|
||||||
|
.content:
|
||||||
|
type: editor
|
||||||
|
label: Content
|
||||||
|
|
||||||
|
# Video specific
|
||||||
|
.video_src:
|
||||||
|
type: filepicker
|
||||||
|
label: Video File
|
||||||
|
folder: 'user://media/videos'
|
||||||
|
accept:
|
||||||
|
- video/*
|
||||||
|
|
||||||
|
.video_poster:
|
||||||
|
type: filepicker
|
||||||
|
label: Video Poster
|
||||||
|
folder: 'user://media/images'
|
||||||
|
accept:
|
||||||
|
- image/*
|
||||||
|
|
||||||
|
# Audio specific
|
||||||
|
.audio_src:
|
||||||
|
type: filepicker
|
||||||
|
label: Audio File
|
||||||
|
folder: 'user://media/audio'
|
||||||
|
accept:
|
||||||
|
- audio/*
|
||||||
|
|
||||||
|
.caption:
|
||||||
|
type: text
|
||||||
|
label: Caption
|
||||||
|
|
||||||
|
# Tabs specific
|
||||||
|
.tabs:
|
||||||
|
type: list
|
||||||
|
label: Tabs
|
||||||
|
fields:
|
||||||
|
.title:
|
||||||
|
type: text
|
||||||
|
label: Tab Title
|
||||||
|
.content:
|
||||||
|
type: editor
|
||||||
|
label: Tab Content
|
||||||
|
|
||||||
|
# Slideshow specific
|
||||||
|
.slides:
|
||||||
|
type: list
|
||||||
|
label: Slides
|
||||||
|
fields:
|
||||||
|
.image:
|
||||||
|
type: filepicker
|
||||||
|
label: Slide Image
|
||||||
|
folder: 'user://media/images'
|
||||||
|
accept:
|
||||||
|
- image/*
|
||||||
|
.caption:
|
||||||
|
type: text
|
||||||
|
label: Slide Caption
|
||||||
|
|
||||||
|
# Media Tab
|
||||||
|
media_tab:
|
||||||
|
type: tab
|
||||||
|
title: Media Files
|
||||||
|
fields:
|
||||||
|
header.media_folder:
|
||||||
|
type: text
|
||||||
|
label: Media Folder
|
||||||
|
help: Folder containing media files for this station
|
||||||
|
placeholder: /media/station-id
|
||||||
27
config/site.yaml
Normal file
27
config/site.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Grav Site Configuration
|
||||||
|
# /user/config/site.yaml
|
||||||
|
|
||||||
|
title: EU Media Guide CMS
|
||||||
|
default_lang: en
|
||||||
|
|
||||||
|
author:
|
||||||
|
name: Admin
|
||||||
|
email: admin@example.com
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
description: Multilingual Media Guide Content Management System
|
||||||
|
|
||||||
|
taxonomies:
|
||||||
|
- category
|
||||||
|
- tag
|
||||||
|
- exhibition
|
||||||
|
- station_type
|
||||||
|
|
||||||
|
# Custom fields for the media guide
|
||||||
|
mediaguide:
|
||||||
|
exhibitions: []
|
||||||
|
station_types:
|
||||||
|
- intro
|
||||||
|
- exhibit
|
||||||
|
- information
|
||||||
|
- navigation
|
||||||
73
config/system.yaml
Normal file
73
config/system.yaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Grav System Configuration
|
||||||
|
# /user/config/system.yaml
|
||||||
|
|
||||||
|
languages:
|
||||||
|
supported:
|
||||||
|
# EU Protocol Order (alphabetical by native name)
|
||||||
|
- bg # български (Bulgarian)
|
||||||
|
- es # español (Spanish)
|
||||||
|
- cs # čeština (Czech)
|
||||||
|
- da # dansk (Danish)
|
||||||
|
- de # Deutsch (German)
|
||||||
|
- et # eesti (Estonian)
|
||||||
|
- el # ελληνικά (Greek)
|
||||||
|
- en # English
|
||||||
|
- fr # français (French)
|
||||||
|
- ga # Gaeilge (Irish)
|
||||||
|
- hr # hrvatski (Croatian)
|
||||||
|
- it # italiano (Italian)
|
||||||
|
- lv # latviešu (Latvian)
|
||||||
|
- lt # lietuvių (Lithuanian)
|
||||||
|
- hu # magyar (Hungarian)
|
||||||
|
- mt # Malti (Maltese)
|
||||||
|
- nl # Nederlands (Dutch)
|
||||||
|
- pl # polski (Polish)
|
||||||
|
- pt # português (Portuguese)
|
||||||
|
- ro # română (Romanian)
|
||||||
|
- sk # slovenčina (Slovak)
|
||||||
|
- sl # slovenščina (Slovenian)
|
||||||
|
- fi # suomi (Finnish)
|
||||||
|
- sv # svenska (Swedish)
|
||||||
|
default_lang: en
|
||||||
|
include_default_lang: true
|
||||||
|
translations: true
|
||||||
|
translations_fallback: true
|
||||||
|
session_store_active: false
|
||||||
|
http_accept_language: true
|
||||||
|
override_locale: false
|
||||||
|
|
||||||
|
pages:
|
||||||
|
theme: quark
|
||||||
|
markdown:
|
||||||
|
extra: true
|
||||||
|
types:
|
||||||
|
- txt
|
||||||
|
- xml
|
||||||
|
- html
|
||||||
|
- htm
|
||||||
|
- json
|
||||||
|
- rss
|
||||||
|
- atom
|
||||||
|
|
||||||
|
cache:
|
||||||
|
enabled: false # Disable during development
|
||||||
|
check:
|
||||||
|
method: file
|
||||||
|
driver: auto
|
||||||
|
prefix: 'g'
|
||||||
|
lifetime: 604800
|
||||||
|
|
||||||
|
debugger:
|
||||||
|
enabled: false
|
||||||
|
shutdown:
|
||||||
|
close_connection: true
|
||||||
|
|
||||||
|
twig:
|
||||||
|
cache: false # Disable during development
|
||||||
|
debug: true
|
||||||
|
auto_reload: true
|
||||||
|
autoescape: false
|
||||||
|
|
||||||
|
assets:
|
||||||
|
css_pipeline: false
|
||||||
|
js_pipeline: false
|
||||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
grav-cms:
|
||||||
|
image: getgrav/grav:latest
|
||||||
|
container_name: mediaguide-cms
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
# Persistent storage for all Grav user content
|
||||||
|
- grav_user:/var/www/html/user
|
||||||
|
# Custom blueprints (mounted from host)
|
||||||
|
- ./blueprints:/var/www/html/user/blueprints
|
||||||
|
# Custom plugins config
|
||||||
|
- ./config:/var/www/html/user/config
|
||||||
|
environment:
|
||||||
|
- GRAV_MULTISITE=false
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
grav_user:
|
||||||
|
driver: local
|
||||||
145
pages/stations/test-module/station.en.md
Normal file
145
pages/stations/test-module/station.en.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
title: Test Module - Welcome
|
||||||
|
station_id: test-welcome
|
||||||
|
station_type: intro
|
||||||
|
exhibition_ref: main-exhibition
|
||||||
|
active: true
|
||||||
|
order: 1
|
||||||
|
|
||||||
|
# Multilingual titles
|
||||||
|
title_translations:
|
||||||
|
en: Welcome to the European Parliament
|
||||||
|
fr: Bienvenue au Parlement européen
|
||||||
|
de: Willkommen im Europäischen Parlament
|
||||||
|
nl: Welkom bij het Europees Parlement
|
||||||
|
it: Benvenuti al Parlamento europeo
|
||||||
|
es: Bienvenidos al Parlamento Europeo
|
||||||
|
|
||||||
|
# Multilingual descriptions
|
||||||
|
description_translations:
|
||||||
|
en: Your interactive guide to European democracy
|
||||||
|
fr: Votre guide interactif de la démocratie européenne
|
||||||
|
de: Ihr interaktiver Führer zur europäischen Demokratie
|
||||||
|
nl: Uw interactieve gids voor Europese democratie
|
||||||
|
it: La tua guida interattiva alla democrazia europea
|
||||||
|
es: Su guía interactiva de la democracia europea
|
||||||
|
|
||||||
|
# Content components
|
||||||
|
components:
|
||||||
|
- type: heading
|
||||||
|
id: welcome-heading
|
||||||
|
level: h1
|
||||||
|
content:
|
||||||
|
en: Welcome to the European Parliament
|
||||||
|
fr: Bienvenue au Parlement européen
|
||||||
|
de: Willkommen im Europäischen Parlament
|
||||||
|
nl: Welkom bij het Europees Parlement
|
||||||
|
|
||||||
|
- type: text
|
||||||
|
id: intro-text
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
<p>Welcome to the heart of European democracy. This interactive guide
|
||||||
|
will help you navigate through the European Parliament building and
|
||||||
|
learn about its institutions.</p>
|
||||||
|
<p>Use the menu to select your preferred language and scan QR codes
|
||||||
|
throughout the building to access specific information.</p>
|
||||||
|
fr: |
|
||||||
|
<p>Bienvenue au cœur de la démocratie européenne. Ce guide interactif
|
||||||
|
vous aidera à naviguer dans le bâtiment du Parlement européen et à
|
||||||
|
découvrir ses institutions.</p>
|
||||||
|
de: |
|
||||||
|
<p>Willkommen im Herzen der europäischen Demokratie. Dieser interaktive
|
||||||
|
Führer hilft Ihnen bei der Navigation durch das Gebäude des Europäischen
|
||||||
|
Parlaments.</p>
|
||||||
|
nl: |
|
||||||
|
<p>Welkom in het hart van de Europese democratie. Deze interactieve gids
|
||||||
|
helpt u door het gebouw van het Europees Parlement te navigeren.</p>
|
||||||
|
|
||||||
|
- type: video
|
||||||
|
id: welcome-video
|
||||||
|
src:
|
||||||
|
en: /media/videos/welcome-en.mp4
|
||||||
|
fr: /media/videos/welcome-fr.mp4
|
||||||
|
de: /media/videos/welcome-de.mp4
|
||||||
|
poster: /media/images/video-poster.jpg
|
||||||
|
caption:
|
||||||
|
en: A brief introduction to the European Parliament
|
||||||
|
fr: Une brève introduction au Parlement européen
|
||||||
|
de: Eine kurze Einführung in das Europäische Parlament
|
||||||
|
|
||||||
|
- type: audio
|
||||||
|
id: welcome-audio
|
||||||
|
src:
|
||||||
|
en: /media/audio/welcome-en.mp3
|
||||||
|
fr: /media/audio/welcome-fr.mp3
|
||||||
|
de: /media/audio/welcome-de.mp3
|
||||||
|
caption:
|
||||||
|
en: Audio welcome message
|
||||||
|
fr: Message d'accueil audio
|
||||||
|
de: Audio-Willkommensnachricht
|
||||||
|
|
||||||
|
- type: tabs
|
||||||
|
id: info-tabs
|
||||||
|
tabs:
|
||||||
|
- title:
|
||||||
|
en: Information
|
||||||
|
fr: Informations
|
||||||
|
de: Information
|
||||||
|
nl: Informatie
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
<p>The European Parliament is one of the EU's law-making bodies.
|
||||||
|
It is directly elected by EU voters every 5 years.</p>
|
||||||
|
fr: |
|
||||||
|
<p>Le Parlement européen est l'un des organes législatifs de l'UE.
|
||||||
|
Il est élu directement par les citoyens européens tous les 5 ans.</p>
|
||||||
|
de: |
|
||||||
|
<p>Das Europäische Parlament ist eines der gesetzgebenden Organe der EU.
|
||||||
|
Es wird alle 5 Jahre direkt von den EU-Bürgern gewählt.</p>
|
||||||
|
- title:
|
||||||
|
en: Opening Hours
|
||||||
|
fr: Horaires d'ouverture
|
||||||
|
de: Öffnungszeiten
|
||||||
|
nl: Openingstijden
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
<p><strong>Monday-Friday:</strong> 9:00 - 18:00</p>
|
||||||
|
<p><strong>Saturday:</strong> 10:00 - 18:00</p>
|
||||||
|
<p><strong>Sunday:</strong> Closed</p>
|
||||||
|
fr: |
|
||||||
|
<p><strong>Lundi-Vendredi:</strong> 9h00 - 18h00</p>
|
||||||
|
<p><strong>Samedi:</strong> 10h00 - 18h00</p>
|
||||||
|
<p><strong>Dimanche:</strong> Fermé</p>
|
||||||
|
- title:
|
||||||
|
en: Services
|
||||||
|
fr: Services
|
||||||
|
de: Dienstleistungen
|
||||||
|
nl: Diensten
|
||||||
|
content:
|
||||||
|
en: |
|
||||||
|
<p>Guided Tours, Information Desk, Cafeteria, Gift Shop, Accessibility Services</p>
|
||||||
|
fr: |
|
||||||
|
<p>Visites guidées, Bureau d'information, Cafétéria, Boutique, Services d'accessibilité</p>
|
||||||
|
|
||||||
|
- type: slideshow
|
||||||
|
id: building-slideshow
|
||||||
|
slides:
|
||||||
|
- image: /media/images/exterior.jpg
|
||||||
|
caption:
|
||||||
|
en: Exterior of the European Parliament building
|
||||||
|
fr: Extérieur du bâtiment du Parlement européen
|
||||||
|
de: Außenansicht des Europäischen Parlaments
|
||||||
|
- image: /media/images/hemicycle.jpg
|
||||||
|
caption:
|
||||||
|
en: The Hemicycle where plenary sessions take place
|
||||||
|
fr: L'hémicycle où se déroulent les sessions plénières
|
||||||
|
de: Das Plenum, in dem die Plenarsitzungen stattfinden
|
||||||
|
- image: /media/images/parliamentarium.jpg
|
||||||
|
caption:
|
||||||
|
en: The Parliamentarium visitors center
|
||||||
|
fr: Le centre des visiteurs Parlamentarium
|
||||||
|
de: Das Besucherzentrum Parlamentarium
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the main content body for the test module.
|
||||||
10
plugins/json-export/blueprints.yaml
Normal file
10
plugins/json-export/blueprints.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: JSON Export
|
||||||
|
version: 1.0.0
|
||||||
|
description: Exports Grav content to JSON format for the React media guide app
|
||||||
|
icon: file-code
|
||||||
|
author:
|
||||||
|
name: Media Guide Team
|
||||||
|
homepage: https://github.com/your-org/mediaguide
|
||||||
|
keywords: json, export, api, multilingual
|
||||||
|
bugs: https://github.com/your-org/mediaguide/issues
|
||||||
|
license: MIT
|
||||||
340
plugins/json-export/json-export.php
Normal file
340
plugins/json-export/json-export.php
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
<?php
|
||||||
|
namespace Grav\Plugin;
|
||||||
|
|
||||||
|
use Grav\Common\Plugin;
|
||||||
|
use RocketTheme\Toolbox\Event\Event;
|
||||||
|
use Grav\Common\Page\Page;
|
||||||
|
use Grav\Common\Grav;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Export Plugin
|
||||||
|
*
|
||||||
|
* Exports content from Grav CMS to JSON format for the React media guide app
|
||||||
|
*/
|
||||||
|
class JsonExportPlugin extends Plugin
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'onPluginsInitialized' => ['onPluginsInitialized', 0]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onPluginsInitialized()
|
||||||
|
{
|
||||||
|
// Check if we're in admin context
|
||||||
|
if ($this->isAdmin()) {
|
||||||
|
$this->enable([
|
||||||
|
'onAdminTaskExecute' => ['onAdminTaskExecute', 0]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable API endpoint
|
||||||
|
$this->enable([
|
||||||
|
'onPagesInitialized' => ['onPagesInitialized', 0]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onPagesInitialized()
|
||||||
|
{
|
||||||
|
$uri = $this->grav['uri'];
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
if (strpos($uri->path(), '/api/') === 0) {
|
||||||
|
$this->handleApiRequest($uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleApiRequest($uri)
|
||||||
|
{
|
||||||
|
$path = $uri->path();
|
||||||
|
$lang = $uri->param('lang') ?? 'en';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
// GET /api/stations - List all stations
|
||||||
|
if ($path === '/api/stations') {
|
||||||
|
echo json_encode($this->getStationsList($lang), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/station/{id} - Get single station
|
||||||
|
if (preg_match('#^/api/station/([a-z0-9-]+)$#', $path, $matches)) {
|
||||||
|
$stationId = $matches[1];
|
||||||
|
$station = $this->getStation($stationId, $lang);
|
||||||
|
if ($station) {
|
||||||
|
echo json_encode($station, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Station not found']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/translations - Get UI translations
|
||||||
|
if ($path === '/api/translations') {
|
||||||
|
echo json_encode($this->getTranslations($lang), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/export - Export all content to JSON files
|
||||||
|
if ($path === '/api/export') {
|
||||||
|
$result = $this->exportAllToJson();
|
||||||
|
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStationsList($lang)
|
||||||
|
{
|
||||||
|
$pages = $this->grav['pages'];
|
||||||
|
$stations = [];
|
||||||
|
|
||||||
|
foreach ($pages->all() as $page) {
|
||||||
|
if ($page->template() === 'station' && $page->header()->active) {
|
||||||
|
$header = $page->header();
|
||||||
|
$stations[] = [
|
||||||
|
'id' => $header->station_id ?? $page->slug(),
|
||||||
|
'type' => $header->station_type ?? 'exhibit',
|
||||||
|
'title' => $this->getTranslatedField($header->title_translations ?? [], $lang),
|
||||||
|
'description' => $this->getTranslatedField($header->description_translations ?? [], $lang),
|
||||||
|
'order' => $header->order ?? 0,
|
||||||
|
'active' => $header->active ?? true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by order
|
||||||
|
usort($stations, fn($a, $b) => $a['order'] <=> $b['order']);
|
||||||
|
|
||||||
|
return $stations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStation($stationId, $lang)
|
||||||
|
{
|
||||||
|
$pages = $this->grav['pages'];
|
||||||
|
|
||||||
|
foreach ($pages->all() as $page) {
|
||||||
|
if ($page->template() === 'station') {
|
||||||
|
$header = $page->header();
|
||||||
|
$id = $header->station_id ?? $page->slug();
|
||||||
|
|
||||||
|
if ($id === $stationId) {
|
||||||
|
return $this->formatStationForLang($page, $lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatStationForLang($page, $lang)
|
||||||
|
{
|
||||||
|
$header = $page->header();
|
||||||
|
|
||||||
|
$station = [
|
||||||
|
'id' => $header->station_id ?? $page->slug(),
|
||||||
|
'type' => $header->station_type ?? 'exhibit',
|
||||||
|
'language' => $lang,
|
||||||
|
'title' => $this->getTranslatedField($header->title_translations ?? [], $lang),
|
||||||
|
'description' => $this->getTranslatedField($header->description_translations ?? [], $lang),
|
||||||
|
'components' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
// Process components
|
||||||
|
if (isset($header->components) && is_array($header->components)) {
|
||||||
|
foreach ($header->components as $component) {
|
||||||
|
$station['components'][] = $this->formatComponent($component, $lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $station;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatComponent($component, $lang)
|
||||||
|
{
|
||||||
|
$formatted = [
|
||||||
|
'id' => $component['id'] ?? uniqid(),
|
||||||
|
'type' => $component['type'] ?? 'text'
|
||||||
|
];
|
||||||
|
|
||||||
|
switch ($component['type']) {
|
||||||
|
case 'heading':
|
||||||
|
$formatted['level'] = $component['level'] ?? 'h2';
|
||||||
|
$formatted['content'] = $this->getTranslatedField($component['content'] ?? [], $lang);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
$formatted['content'] = $this->getTranslatedField($component['content'] ?? [], $lang);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'video':
|
||||||
|
$formatted['src'] = $this->getTranslatedField($component['src'] ?? [], $lang);
|
||||||
|
$formatted['poster'] = $component['poster'] ?? null;
|
||||||
|
$formatted['caption'] = $this->getTranslatedField($component['caption'] ?? [], $lang);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
$formatted['src'] = $this->getTranslatedField($component['src'] ?? [], $lang);
|
||||||
|
$formatted['caption'] = $this->getTranslatedField($component['caption'] ?? [], $lang);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tabs':
|
||||||
|
$formatted['tabs'] = [];
|
||||||
|
if (isset($component['tabs']) && is_array($component['tabs'])) {
|
||||||
|
foreach ($component['tabs'] as $tab) {
|
||||||
|
$formatted['tabs'][] = [
|
||||||
|
'title' => $this->getTranslatedField($tab['title'] ?? [], $lang),
|
||||||
|
'content' => $this->getTranslatedField($tab['content'] ?? [], $lang)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'slideshow':
|
||||||
|
$formatted['slides'] = [];
|
||||||
|
if (isset($component['slides']) && is_array($component['slides'])) {
|
||||||
|
foreach ($component['slides'] as $slide) {
|
||||||
|
$formatted['slides'][] = [
|
||||||
|
'image' => $slide['image'] ?? '',
|
||||||
|
'caption' => $this->getTranslatedField($slide['caption'] ?? [], $lang)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTranslatedField($field, $lang, $fallback = 'en')
|
||||||
|
{
|
||||||
|
if (is_string($field)) {
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($field)) {
|
||||||
|
return $field[$lang] ?? $field[$fallback] ?? reset($field) ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTranslations($lang)
|
||||||
|
{
|
||||||
|
// UI translations - these should be stored in config or separate files
|
||||||
|
$translations = [
|
||||||
|
'en' => [
|
||||||
|
'appTitle' => 'EU Media Guide',
|
||||||
|
'scanQR' => 'Scan QR',
|
||||||
|
'selectLanguage' => 'Select Language',
|
||||||
|
'contentLibrary' => 'Content Library',
|
||||||
|
'noContent' => 'Scan a QR code or select content from the menu',
|
||||||
|
'errorLoading' => 'Failed to load content. Please try again.',
|
||||||
|
'playAudio' => 'Play Audio',
|
||||||
|
'pauseAudio' => 'Pause Audio',
|
||||||
|
'next' => 'Next',
|
||||||
|
'previous' => 'Previous',
|
||||||
|
'closeMenu' => 'Close Menu',
|
||||||
|
'openMenu' => 'Open Menu',
|
||||||
|
'loading' => 'Loading...'
|
||||||
|
],
|
||||||
|
'fr' => [
|
||||||
|
'appTitle' => 'Guide Média UE',
|
||||||
|
'scanQR' => 'Scanner QR',
|
||||||
|
'selectLanguage' => 'Choisir la langue',
|
||||||
|
'contentLibrary' => 'Bibliothèque de contenu',
|
||||||
|
'noContent' => 'Scannez un code QR ou sélectionnez du contenu dans le menu',
|
||||||
|
'errorLoading' => 'Échec du chargement. Veuillez réessayer.',
|
||||||
|
'playAudio' => 'Lire l\'audio',
|
||||||
|
'pauseAudio' => 'Pause audio',
|
||||||
|
'next' => 'Suivant',
|
||||||
|
'previous' => 'Précédent',
|
||||||
|
'closeMenu' => 'Fermer le menu',
|
||||||
|
'openMenu' => 'Ouvrir le menu',
|
||||||
|
'loading' => 'Chargement...'
|
||||||
|
],
|
||||||
|
'de' => [
|
||||||
|
'appTitle' => 'EU Media Guide',
|
||||||
|
'scanQR' => 'QR scannen',
|
||||||
|
'selectLanguage' => 'Sprache wählen',
|
||||||
|
'contentLibrary' => 'Inhaltsbibliothek',
|
||||||
|
'noContent' => 'Scannen Sie einen QR-Code oder wählen Sie Inhalte aus dem Menü',
|
||||||
|
'errorLoading' => 'Laden fehlgeschlagen. Bitte versuchen Sie es erneut.',
|
||||||
|
'playAudio' => 'Audio abspielen',
|
||||||
|
'pauseAudio' => 'Audio pausieren',
|
||||||
|
'next' => 'Weiter',
|
||||||
|
'previous' => 'Zurück',
|
||||||
|
'closeMenu' => 'Menü schließen',
|
||||||
|
'openMenu' => 'Menü öffnen',
|
||||||
|
'loading' => 'Laden...'
|
||||||
|
],
|
||||||
|
'nl' => [
|
||||||
|
'appTitle' => 'EU Media Gids',
|
||||||
|
'scanQR' => 'QR scannen',
|
||||||
|
'selectLanguage' => 'Taal selecteren',
|
||||||
|
'contentLibrary' => 'Inhoudsbibliotheek',
|
||||||
|
'noContent' => 'Scan een QR-code of selecteer inhoud uit het menu',
|
||||||
|
'errorLoading' => 'Laden mislukt. Probeer het opnieuw.',
|
||||||
|
'playAudio' => 'Audio afspelen',
|
||||||
|
'pauseAudio' => 'Audio pauzeren',
|
||||||
|
'next' => 'Volgende',
|
||||||
|
'previous' => 'Vorige',
|
||||||
|
'closeMenu' => 'Menu sluiten',
|
||||||
|
'openMenu' => 'Menu openen',
|
||||||
|
'loading' => 'Laden...'
|
||||||
|
]
|
||||||
|
// Add more languages as needed
|
||||||
|
];
|
||||||
|
|
||||||
|
return $translations[$lang] ?? $translations['en'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exportAllToJson()
|
||||||
|
{
|
||||||
|
$exportPath = $this->grav['locator']->findResource('user://') . '/export/';
|
||||||
|
|
||||||
|
if (!is_dir($exportPath)) {
|
||||||
|
mkdir($exportPath, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$languages = $this->grav['config']->get('system.languages.supported', ['en']);
|
||||||
|
$exported = [];
|
||||||
|
|
||||||
|
foreach ($languages as $lang) {
|
||||||
|
// Export stations
|
||||||
|
$stations = $this->getStationsList($lang);
|
||||||
|
file_put_contents(
|
||||||
|
$exportPath . "stations.{$lang}.json",
|
||||||
|
json_encode($stations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Export each station content
|
||||||
|
foreach ($stations as $station) {
|
||||||
|
$content = $this->getStation($station['id'], $lang);
|
||||||
|
if ($content) {
|
||||||
|
file_put_contents(
|
||||||
|
$exportPath . "{$station['id']}.{$lang}.json",
|
||||||
|
json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export translations
|
||||||
|
$translations = $this->getTranslations($lang);
|
||||||
|
file_put_contents(
|
||||||
|
$exportPath . "translations.{$lang}.json",
|
||||||
|
json_encode($translations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
|
||||||
|
$exported[] = $lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'exported_languages' => $exported,
|
||||||
|
'path' => $exportPath
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
plugins/json-export/json-export.yaml
Normal file
24
plugins/json-export/json-export.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: JSON Export
|
||||||
|
version: 1.0.0
|
||||||
|
description: Exports Grav content to JSON format for the React media guide app
|
||||||
|
icon: file-code
|
||||||
|
author:
|
||||||
|
name: Media Guide Team
|
||||||
|
homepage: https://github.com/your-org/mediaguide
|
||||||
|
keywords: json, export, api, multilingual
|
||||||
|
bugs: https://github.com/your-org/mediaguide/issues
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
form:
|
||||||
|
validation: strict
|
||||||
|
fields:
|
||||||
|
enabled:
|
||||||
|
type: toggle
|
||||||
|
label: Plugin status
|
||||||
|
highlight: 1
|
||||||
|
default: 1
|
||||||
|
options:
|
||||||
|
1: Enabled
|
||||||
|
0: Disabled
|
||||||
|
validate:
|
||||||
|
type: bool
|
||||||
Reference in New Issue
Block a user