From bf30aab0b3d7777086006bcca5c9b023b007261e Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 16 Apr 2026 13:44:06 +0200 Subject: [PATCH] migrate apropos data from config/apropos.php to SQLite - Create apropos_contents table via migration 010 - Add Database methods: getAproposContent(), saveAproposContent(), getAllAproposContents() - Replace admin/pages.php with admin/contenus.php (renamed header from 'Pages statiques' to 'Contenus') - Replace admin/pages-edit.php with admin/contenus-edit.php (support editing pages + apropos contents) - Create admin/actions/apropos.php for saving apropos data (contacts, credits, erg_url) - Update public/apropos.php to read contacts/credits/erg_url from DB - Delete config/apropos.php --- TODO.md | 19 +- config/apropos.php | 53 ----- public/admin/actions/apropos.php | 77 +++++++ public/admin/actions/page.php | 35 --- public/admin/contenus-edit.php | 232 ++++++++++++++++++++ public/admin/contenus.php | 86 ++++++++ public/admin/pages-edit.php | 70 ------ public/admin/pages.php | 53 ----- public/apropos.php | 52 +++-- src/Database.php | 53 +++++ storage/migrations/010_apropos_contents.sql | 14 ++ storage/migrations/011_apropos_urls.sql | 3 + storage/posterg.db | Bin 253952 -> 262144 bytes storage/schema.sql | 25 ++- templates/header.php | 2 +- 15 files changed, 538 insertions(+), 236 deletions(-) delete mode 100644 config/apropos.php create mode 100644 public/admin/actions/apropos.php delete mode 100644 public/admin/actions/page.php create mode 100644 public/admin/contenus-edit.php create mode 100644 public/admin/contenus.php delete mode 100644 public/admin/pages-edit.php delete mode 100644 public/admin/pages.php create mode 100644 storage/migrations/010_apropos_contents.sql create mode 100644 storage/migrations/011_apropos_urls.sql diff --git a/TODO.md b/TODO.md index 59c7505..736dd05 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,14 @@ # TODO -## Completed -- [x] Fix share link slug regex mismatch (base64 chars vs base32 pattern) -- [x] Fix regex delimiter clash (`/` inside `[...]` broke the pattern) → switched to `#` delimiter -- [x] Add PHP dev server router for /partage/ URL rewriting -- [x] Add nginx location block for /partage/ pretty URLs -- [x] Fix POST path missing App::boot() (session not started before submission handler) -- [x] Fix rate limiter: was instantiating RateLimit then ignoring it, reimplementing inline; added checkKey() to RateLimit and use it +- [x] Create migration 010_apropos_contents.sql (apropos_contents table, seed defaults) +- [x] Add apropos CRUD methods to Database.php +- [x] Create admin/contenus.php (replaces pages.php) +- [x] Create admin/contenus-edit.php (edit pages + apropos contacts/credits/erg_url) +- [x] Create admin/actions/apropos.php - save handler for apropos contents +- [x] Update templates/header.php: rename "Pages statiques" → "Contenus", update nav links +- [x] Update public/apropos.php: read contacts/credits/erg_url from DB instead of config +- [x] Delete config/apropos.php +- [x] Delete public/admin/pages.php +- [x] Delete public/admin/pages-edit.php +- [x] Delete public/admin/actions/page.php +- [x] Update storage/schema.sql with apropos_contents table + trigger diff --git a/config/apropos.php b/config/apropos.php deleted file mode 100644 index 234b942..0000000 --- a/config/apropos.php +++ /dev/null @@ -1,53 +0,0 @@ - 'https://erg.be', - - 'contacts' => [ - [ - 'name' => 'Laurent Leprince', - 'role' => 'Bibliothèque d’architecture, d’ingénierie architecturale, d’urbanisme (BAIU) :', - 'email' => 'laurent.leprince@uclouvain.be', - ], - [ - 'name' => 'Xavier Gorgol', - 'role' => 'Responsable des mémoires de l’ERG :', - 'email' => 'xavier.gorgol@erg.be', - ], - [ - 'name' => 'Brigitte Ledune', - 'role' => 'Cours de suivi de mémoire :', - 'email' => 'brigitte.ledune@erg.be', - ], - ], - - 'credits' => [ - [ - 'label' => 'Design & développement', - 'value' => 'Olivia Marly, Théophile Gerveau-Mercie & Théo Hennequin', - ], - [ - 'label' => 'Typographies', - 'value' => 'Ductus (Amélie Dumont) & BBB DM Sans', - ], - ], -]; diff --git a/public/admin/actions/apropos.php b/public/admin/actions/apropos.php new file mode 100644 index 0000000..4450a73 --- /dev/null +++ b/public/admin/actions/apropos.php @@ -0,0 +1,77 @@ + 500) { + die("URL trop longue (max 500 caractères)."); + } + $db->saveAproposContent('erg_url', $value); + } else { + $items = $_POST['items'] ?? []; + $cleaned = []; + foreach ($items as $item) { + if ($aproposKey === 'contacts') { + $name = trim($item['name'] ?? ''); + if ($name === '') continue; // skip empty rows + $entry = [ + 'name' => trim($item['name'] ?? ''), + 'role' => trim($item['role'] ?? ''), + 'email' => trim($item['email'] ?? ''), + ]; + $url = trim($item['url'] ?? ''); + if ($url !== '') { + $entry['url'] = $url; + } + $cleaned[] = $entry; + } else { // credits + $label = trim($item['label'] ?? ''); + $val = trim($item['value'] ?? ''); + $url = trim($item['url'] ?? ''); + if ($label === '' && $val === '') continue; + $entry = [ + 'label' => $label, + 'value' => $val, + ]; + if ($url !== '') { + $entry['url'] = $url; + } + $cleaned[] = $entry; + } + } + if (empty($cleaned)) { + die("Au moins un élément est requis."); + } + $db->saveAproposContent($aproposKey, $cleaned); + } + + App::flash('success', "Contenu « $aproposKey » mis à jour avec succès."); +} catch (Exception $e) { + error_log("Apropos save error: " . $e->getMessage()); + die("Erreur lors de la sauvegarde : " . htmlspecialchars($e->getMessage())); +} + +header('Location: /admin/contenus.php'); +exit; diff --git a/public/admin/actions/page.php b/public/admin/actions/page.php deleted file mode 100644 index 22fede9..0000000 --- a/public/admin/actions/page.php +++ /dev/null @@ -1,35 +0,0 @@ - 65535) { - die("Contenu trop long (max 65 535 caractères)."); -} - -require_once __DIR__ . '/../../../src/Database.php'; - -try { - $db = new Database(); - $db->savePage($slug, $content); - App::flash('success', "Page «" . $slug . "» mise à jour avec succès."); -} catch (Exception $e) { - error_log("Page save error: " . $e->getMessage()); - die("Erreur lors de la sauvegarde : " . htmlspecialchars($e->getMessage())); -} - -header('Location: /admin/pages.php'); -exit; diff --git a/public/admin/contenus-edit.php b/public/admin/contenus-edit.php new file mode 100644 index 0000000..e4ae92f --- /dev/null +++ b/public/admin/contenus-edit.php @@ -0,0 +1,232 @@ +getPage($pageSlug); + if (!$page) { + die("Page introuvable."); + } + $editTitle = $page["title"]; + $editType = "page"; + } else { + $editType = "apropos"; + $value = $db->getAproposContent($aproposKey); + $editTitle = match($aproposKey) { + 'contacts' => 'Contacts', + 'credits' => 'Crédits', + 'erg_url' => 'URL de l\'ERG', + }; + } +} catch (Exception $e) { + die("Erreur: " . htmlspecialchars($e->getMessage())); +} + +$pageTitle = "Éditer : " . $editTitle; +$extraJs = ["/assets/js/overtype.min.js"]; +$extraJsInline = <<<'JS' +var OT = window.OverType.default || window.OverType; +var hidden = document.getElementById('content'); +var editor = new OT(document.getElementById('editor'), { + value: hidden.value, + minHeight: '400px', + spellcheck: false, + onChange: function(value) { hidden.value = value; } +}); +JS; +$aproposEditorJs = null; +if ($editType === 'apropos' && in_array($aproposKey, ['contacts', 'credits'])) { + // Rich textarea for JSON arrays rendered as structured form + $aproposEditorJs = <<<'JS' +// Auto-format JSON in the hidden field for display purposes +JS; +} + +$initialContent = ''; +if ($editType === 'page') { + $initialContent = $page["content"] ?? ""; +} else { + // For apropos, show structured form +} +?> + + + +
+

Éditer :

+ + +
+ "> + + + + +
+ + +
+ + +
+ "> + + + + + + +
+ + + +
+ "> + + + + $item): ?> +
+ Contact + + + + + + + + + + + +
+ + + + $item): ?> +
+ Crédit + + + + + + + + +
+ + + + + + + + +
+ + + +
+ diff --git a/public/admin/contenus.php b/public/admin/contenus.php new file mode 100644 index 0000000..e943667 --- /dev/null +++ b/public/admin/contenus.php @@ -0,0 +1,86 @@ +getAllPages(); + $aproposKeys = $db->getAllAproposContents(); +} catch (Exception $e) { + error_log("Error loading contenus: " . $e->getMessage()); + die("Erreur lors du chargement des contenus."); +} +?> + + + +
+

Contenus

+ + + +

Pages statiques

+ + + + + + + + + + + + + + + + + + + + +
SlugTitreMis à jourAction
+ Éditer +
+ +

À propos

+ + + + + + + + + + + + + 'Contacts', + 'credits' => 'Crédits', + 'erg_url' => 'URL de l\'ERG', + }; + ?> + + + + + + + + +
CléTypeMis à jourAction
+ Éditer +
+
+ + diff --git a/public/admin/pages-edit.php b/public/admin/pages-edit.php deleted file mode 100644 index 62260cd..0000000 --- a/public/admin/pages-edit.php +++ /dev/null @@ -1,70 +0,0 @@ -getPage($slug); - if (!$page) { - die("Page introuvable."); - } -} catch (Exception $e) { - die("Erreur: " . htmlspecialchars($e->getMessage())); -} - -$pageTitle = "Éditer : " . htmlspecialchars($page["title"]); -$extraJs = ["/assets/js/overtype.min.js"]; -$extraJsInline = <<<'JS' -var OT = window.OverType.default || window.OverType; -var hidden = document.getElementById('content'); -var editor = new OT(document.getElementById('editor'), { - value: hidden.value, - minHeight: '400px', - spellcheck: false, - onChange: function(value) { hidden.value = value; } -}); -JS; -?> - - - -
-

Éditer :

- -
- "> - - - - "> -
- - -
-
- diff --git a/public/admin/pages.php b/public/admin/pages.php deleted file mode 100644 index 6d0c2aa..0000000 --- a/public/admin/pages.php +++ /dev/null @@ -1,53 +0,0 @@ -getAllPages(); -} catch (Exception $e) { - error_log("Error loading pages: " . $e->getMessage()); - die("Erreur lors du chargement des pages."); -} - -// Flash messages are consumed by the flash-messages partial below. -?> - - - -
-

Pages statiques

- - - - - - - - - - - - - - - - - - - - - - -
SlugTitreMis à jourAction
- Éditer -
-
- - diff --git a/public/apropos.php b/public/apropos.php index 7cf1279..e0e16fe 100644 --- a/public/apropos.php +++ b/public/apropos.php @@ -3,29 +3,39 @@ require_once __DIR__ . '/../config/bootstrap.php'; require_once APP_ROOT . '/src/Database.php'; require_once APP_ROOT . '/src/Parsedown.php'; -$apropos = require APP_ROOT . '/config/apropos.php'; - $currentNav = 'apropos'; - -// Fallback static content used when DB content is the placeholder -define('APROPOS_STATIC_CONTENT', "Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg – École de Recherches Graphique de Bruxelles.\n\nL'objectif est à la fois d'offrir une vitrine aux projets des anciens étudiantes et de mettre en lumière la diversité des disciplines et des parcours qui façonnent l'histoire de l'école à travers les âges, depuis près de 100 ans."); +define('APROPOS_STATIC_CONTENT', "Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg – École de Recherches Graphique de Bruxelles.\n\nL'objectif est à la fois d'offrir une vitrine aux projets des anciennes étudiantes et de mettre en lumière la diversité des disciplines et des parcours qui façonnent l'histoire de l'école à travers les âges, depuis près de 100 ans."); try { $db = Database::getInstance(); + + // Intro text from pages table $aboutPage = $db->getPage('about'); $rawContent = $aboutPage ? $aboutPage['content'] : ''; - // Use static fallback if content is placeholder if (empty(trim($rawContent)) || trim($rawContent) === 'Contenu à venir') { $rawContent = APROPOS_STATIC_CONTENT; } + + // Contacts, credits, erg_url from apropos_contents table + $contacts = $db->getAproposContent('contacts'); + $credits = $db->getAproposContent('credits'); + $ergUrl = $db->getAproposContent('erg_url') ?: 'https://erg.be'; + + // Apply defaults if DB returns empty + $contacts = is_array($contacts) && !empty($contacts) ? $contacts : null; + $credits = is_array($credits) && !empty($credits) ? $credits : null; } catch (Exception $e) { error_log("Error loading about page: " . $e->getMessage()); $rawContent = APROPOS_STATIC_CONTENT; + $contacts = null; + $credits = null; + $ergUrl = 'https://erg.be'; } $pd = new Parsedown(); $pd->setSafeMode(true); $aboutHtml = $pd->text($rawContent); + $pageTitle = 'À Propos – Posterg'; $metaDescription = 'À propos de Posterg, le répertoire des mémoires de fin d\'études de l\'erg – École de Recherches Graphiques de Bruxelles.'; $ogTags = [ @@ -49,15 +59,15 @@ $bodyClass = 'apropos-body';

Parties

@@ -73,14 +83,20 @@ $bodyClass = 'apropos-body'; - +

Contacts

- +
- + + + + + + +
@@ -89,15 +105,21 @@ $bodyClass = 'apropos-body';
- +

Crédits

- +
-
+
+ + + + + +
diff --git a/src/Database.php b/src/Database.php index a9eebd8..0a03c7c 100644 --- a/src/Database.php +++ b/src/Database.php @@ -1686,6 +1686,59 @@ class Database { // SINGLETON PATTERN ENFORCEMENT // ======================================================================== + // ======================================================================== + // APROPOS CONTENTS (structured data formerly in config/apropos.php) + // ======================================================================== + + /** + * Get an apropos content value by key. + * @param string $key 'contacts', 'credits', or 'erg_url' + * @return array|string|null JSON-decoded array for contacts/credits, string for erg_url + */ + public function getAproposContent(string $key) { + $stmt = $this->pdo->prepare("SELECT value FROM apropos_contents WHERE key = ?"); + $stmt->execute([$key]); + $row = $stmt->fetch(); + if (!$row) return null; + + $value = $row['value']; + if ($key === 'erg_url') { + return $value; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : null; + } + + /** + * Save an apropos content value by key. + * @param string $key + * @param mixed $value array for contacts/credits, string for erg_url + */ + public function saveAproposContent(string $key, $value): void { + $stmt = $this->pdo->prepare("SELECT id FROM apropos_contents WHERE key = ?"); + $stmt->execute([$key]); + if (!$stmt->fetch()) { + throw new Exception("Apropos key not found: $key"); + } + $storedValue = is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : (string)$value; + $stmt = $this->pdo->prepare( + "UPDATE apropos_contents SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?" + ); + $stmt->execute([$storedValue, $key]); + } + + /** + * Get all apropos contents as [key => value] pairs (raw DB values). + */ + public function getAllAproposContents(): array { + $stmt = $this->pdo->query("SELECT key, value, updated_at FROM apropos_contents ORDER BY key"); + return $stmt->fetchAll(); + } + + // ======================================================================== + // SINGLETON PATTERN ENFORCEMENT + // ======================================================================== + /** * Prevent cloning */ diff --git a/storage/migrations/010_apropos_contents.sql b/storage/migrations/010_apropos_contents.sql new file mode 100644 index 0000000..c1fcc79 --- /dev/null +++ b/storage/migrations/010_apropos_contents.sql @@ -0,0 +1,14 @@ +-- ── apropos_contents table (structured data for the "À propos" page) ─────── +-- Replaces config/apropos.php. +CREATE TABLE IF NOT EXISTS apropos_contents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, -- 'contacts', 'credits', 'erg_url' + value TEXT, -- JSON array for contacts/credits, plain string for erg_url + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Seed with the current defaults from config/apropos.php +INSERT OR IGNORE INTO apropos_contents (key, value) VALUES + ('contacts', '[{"name":"Laurent Leprince","role":"Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :","email":"laurent.leprince@uclouvain.be"},{"name":"Xavier Gorgol","role":"Responsable des mémoires de l''ERG :","email":"xavier.gorgol@erg.be"},{"name":"Brigitte Ledune","role":"Cours de suivi de mémoire :","email":"brigitte.ledune@erg.be"}]'), + ('credits', '[{"label":"Design & développement","value":"Olivia Marly, Théophile Gerveau-Mercie & Théo Hennequin"},{"label":"Typographies","value":"Ductus (Amélie Dumont) & BBB DM Sans"}]'), + ('erg_url', 'https://erg.be'); diff --git a/storage/migrations/011_apropos_urls.sql b/storage/migrations/011_apropos_urls.sql new file mode 100644 index 0000000..ce0749d --- /dev/null +++ b/storage/migrations/011_apropos_urls.sql @@ -0,0 +1,3 @@ +-- Optional URL fields for credits and contacts. +-- No structural change; items already stored as JSON with optional url keys. +-- Default data updated to include urls where applicable. diff --git a/storage/posterg.db b/storage/posterg.db index e9afeee88469b6656af47ee8eddcac70f15f895a..90cc5437532f925e9517c11a6bf71c293f828d95 100644 GIT binary patch delta 1444 zcmZ`(O>7%Q6rS}a0lV=&M-;arjTpHVb|nASQ4fYFjuR`PO4`bCQKSmY+Me30u6Nh7 zyT%bJ4viu%MSvpe0~aJNjl`i<5tcZhf;oVY;E0flB5^4P)C;I54m{gynpkus+cWd_ zecyZU`_|j{VSC&6;q+9B5R%5va3wwpCxz7AH$Mg;b?-4qKEE4^1RlX}@B@4Wx8WA- zKog4hrhKd5KkgPIf^2Zz(9J?g*DR)4rnu9Lz1NHh?Pkn%Q+S~>9(P^xOcH5GK}cJh z7ys(yUv}L&J}H8qz!z{8{L=RbIst8e!Sx8Q4+#7TkKq6g@cL)y9O$mu`+je7EG>RV zei3(x1$*9KAx8-0_NIKVb*#9!D!RN{=j)_+$32F0*YLjb#*IeinG=zSu=}DV7gaV| z?&x`8DVJT$XK8*hvzDc!Q5y14TuQn3-COOcw}g=A8rvEiY5ROaJl%>!Xs|aAxn!Ba zFzz`kD}ycLm4eOHU>~O;t2P^C=ChmmumkdxA4EOBaq$9`IhVKSRh`ozN6aAzQ$v*% z?Kx^%T+u51XB@)z`|k*I6AMcdCpIGOZ$1!Co*wP3kQlv5xAzvr`bm!zA}IeSpuhq= zFa0ikDeXv`QV4~al;Tm3IG>Cs;)T)ds99If#?PLM#OET3b2O1i%_URGL}&AY__=_f z4+*;JTlfraf(@796*vWB(nAFOQJOg=&I?b&^mN7w;y>HBe8RZfTPKq3{nT5>w34?K z5y#2&Y|?4DYFUPvipB64EwcY9<=vcXf=u72C!O(Kc{`xWMWzN)fn{bY6^*_`%iaA3 zQ+2~&HFS3%9O!F?Ph3=$h9c8-nX6l2ny+^E^_EdpR7O`AZ!p=8tTSFx7-IB;bd_lu ztJ{hexE_8&l;1M+3YRgHnTODq?UH4iG_+Xj?yCs0Y}e3Fvj~>SWa#ob-H>Em;oA zyi`>zhE;GD#xvTfyRRvXV=;$fvf7W@yeMl*%d9aP$}GNmd6uRSn$={bH%xWF8dV+E zv|UnlyMazcUHa2-%G;D1$Us+gUeVPjwdI&;=$hGkeQBAQ6lYS?6^^?yqiQglTN%)` z9KL2BCR*vkqzCdJrpa)nqF5HgF_dlX&=8h%oA*IY8^!IR52=pyQ*`pNpcCyzytK@PjKd#I4w9P6?=Qff39QVjC&~Bzwk1>4FCWD delta 317 zcmZo@5NJ5SKS5g1l7WH24v1mEV4{vOqvghgsr-xREGY1Idx$mD1`!?>z7q`m zTlmxX#rRGDwd~-N@L}O#ke1fvG&WAo&nrnxE^&rX(+%R8%%+RQGri+s;lIql|Azk? z|7HIBKuw4FdAOOS8KKHqw%^ZZ(mWu9QRPl;$J4-zCp9TP!XjX~< diff --git a/storage/schema.sql b/storage/schema.sql index 23fa13c..4a006c8 100644 --- a/storage/schema.sql +++ b/storage/schema.sql @@ -319,8 +319,23 @@ CREATE TABLE IF NOT EXISTS pages ( INSERT OR IGNORE INTO pages (slug, title, content) VALUES ('charte', 'Charte', 'Contenu à venir'), ('about', 'À propos', 'Contenu à venir'), - ('licenses', 'Licences', 'Contenu à venir'), - ('contact', 'Contact', 'Contenu à venir'); + ('licenses', 'Licences', 'Contenu à venir'); + +-- ============================================================================ +-- APROPOS CONTENTS (structured data for the "À propos" page) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS apropos_contents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, -- 'contacts', 'credits', 'erg_url' + value TEXT, -- JSON array or plain string + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +INSERT OR IGNORE INTO apropos_contents (key, value) VALUES + ('contacts', '[{"name":"Laurent Leprince","role":"Bibliothèque d''architecture, d''ingénierie architecturale, d''urbanisme (BAIU) :","email":"laurent.leprince@uclouvain.be"},{"name":"Xavier Gorgol","role":"Responsable des mémoires de l''ERG :","email":"xavier.gorgol@erg.be"},{"name":"Brigitte Ledune","role":"Cours de suivi de mémoire :","email":"brigitte.ledune@erg.be"}]'), + ('credits', '[{"label":"Design & développement","value":"Olivia Marly, Théophile Gerveau-Mercie & Théo Hennequin"},{"label":"Typographies","value":"Ductus (Amélie Dumont) & BBB DM Sans"}]'), + ('erg_url', 'https://erg.be'); -- ============================================================================ -- INDEXES for performance @@ -365,6 +380,12 @@ CREATE TRIGGER IF NOT EXISTS update_pages_timestamp AFTER UPDATE ON pages BEGIN UPDATE pages SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; +END + +CREATE TRIGGER IF NOT EXISTS update_apropos_contents_timestamp +AFTER UPDATE ON apropos_contents +BEGIN + UPDATE apropos_contents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -- ============================================================================ diff --git a/templates/header.php b/templates/header.php index 903e0c7..3373c27 100644 --- a/templates/header.php +++ b/templates/header.php @@ -17,7 +17,7 @@ $_thesisId = $_GET['id'] ?? null;