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