diff --git a/.gitignore b/.gitignore index 7774c57..88ebe6e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ app/storage/test.db ### Logs ### error.log +app/storage/logs/*.log +!app/storage/logs/.gitkeep app/storage/maintenance.flag app/storage/cache/* !app/storage/cache/.gitkeep diff --git a/TODO.md b/TODO.md index dab47a2..bb754f5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,20 @@ # XAMXAM TODO +## Merge apropos editables into À propos page + remove charte + source code URL +- [x] `actions/apropos.php` — only `contacts`; removed credits, erg_url +- [x] `actions/page.php` — remove `charte` from allowed slugs +- [x] `contenus.php` (front controller) — filter pages to only show `about` + `licenses` +- [x] `templates/admin/contenus.php` — restored "Pages statiques" table +- [x] `contenus-edit.php` (front controller) — `about` slug loads contacts for unified edit +- [x] `templates/admin/contenus-edit.php` — `about_page` type: Markdown editor + contacts on one page +- [x] `templates/admin/apropos-groups-form.php` — reusable partial for contacts +- [x] `Database.php` — simplified getAproposContent/saveAproposContent (contacts-only JSON) +- [x] `storage/schema.sql` — removed credits, erg_url; only contacts remains +- [x] `AboutController.php` — removed credits, sourceCode DB loading +- [x] `templates/public/about.php` — hardcoded source code URL, hardcoded credits HTML +- [x] `apropos.css` — `.apropos-toc-source` styles +- [x] `.gitignore` — ignore `app/storage/logs/*.log` + ## Duplicate TFE submission prevention (fixes) - [x] `DuplicateThesisException` — typed exception carrying existing thesis metadata - [x] `Database::findDuplicateThesis()` — year + author + normalised-title matching (exact, prefix, Levenshtein ≤10%) diff --git a/app/public/admin/actions/apropos.php b/app/public/admin/actions/apropos.php index 61c9c92..e609641 100644 --- a/app/public/admin/actions/apropos.php +++ b/app/public/admin/actions/apropos.php @@ -1,7 +1,7 @@ $text]; - $url = trim($entry['url'] ?? ''); - if ($url !== '') $e['url'] = $url; - $entries[] = $e; - } - if (empty($entries)) continue; - $cleaned[] = ['label' => $label, 'entries' => $entries]; - } else { // contacts - $role = trim($group['role'] ?? ''); - if ($role === '') continue; - $entries = []; - foreach ($group['entries'] ?? [] as $entry) { - $text = trim($entry['text'] ?? ''); - if ($text === '') continue; - $e = [ - 'text' => $text, - 'email' => trim($entry['email'] ?? ''), - ]; - $url = trim($entry['url'] ?? ''); - if ($url !== '') $e['url'] = $url; - $entries[] = $e; - } - if (empty($entries)) continue; - $cleaned[] = ['role' => $role, 'entries' => $entries]; + $role = trim($group['role'] ?? ''); + if ($role === '') continue; + $entries = []; + foreach ($group['entries'] ?? [] as $entry) { + $text = trim($entry['text'] ?? ''); + if ($text === '') continue; + $e = [ + 'text' => $text, + 'email' => trim($entry['email'] ?? ''), + ]; + $url = trim($entry['url'] ?? ''); + if ($url !== '') $e['url'] = $url; + $entries[] = $e; } + if (empty($entries)) continue; + $cleaned[] = ['role' => $role, 'entries' => $entries]; } if (empty($cleaned)) { diff --git a/app/public/admin/actions/page.php b/app/public/admin/actions/page.php index e9ded20..efd8d58 100644 --- a/app/public/admin/actions/page.php +++ b/app/public/admin/actions/page.php @@ -13,7 +13,7 @@ if (!isset($_POST['csrf_token'], $_SESSION['csrf_token']) exit; } -$allowedSlugs = ['about', 'licenses', 'charte']; +$allowedSlugs = ['about', 'licenses']; $slug = $_POST['slug'] ?? ''; $content = $_POST['content'] ?? ''; diff --git a/app/public/admin/contenus-edit.php b/app/public/admin/contenus-edit.php index 2cdb379..2ef536f 100644 --- a/app/public/admin/contenus-edit.php +++ b/app/public/admin/contenus-edit.php @@ -9,8 +9,8 @@ if (empty($_SESSION["csrf_token"])) { $_SESSION["csrf_token"] = bin2hex(random_bytes(32)); } -$allowedPageSlugs = ["about", "licenses", "charte"]; -$allowedApropos = ["contacts", "credits"]; +$allowedPageSlugs = ["about", "licenses"]; +$allowedApropos = ["contacts"]; $pageSlug = $_GET["slug"] ?? ""; $aproposKey = $_GET["apropos"] ?? ""; @@ -40,7 +40,13 @@ try { die("Page introuvable."); } $editTitle = $page["title"]; - $editType = "page"; + if ($pageSlug === 'about') { + $editType = 'about_page'; + $aboutContacts = $db->getAproposContent('contacts'); + $aboutContacts = is_array($aboutContacts) ? $aboutContacts : []; + } else { + $editType = "page"; + } } elseif ($formHelpKey) { $editType = "form_help"; $formHelpContent = $db->getFormHelpBlock($formHelpKey); @@ -50,7 +56,6 @@ try { $value = $db->getAproposContent($aproposKey); $editTitle = match($aproposKey) { 'contacts' => 'Contacts', - 'credits' => 'Crédits', }; } } catch (Exception $e) { @@ -58,8 +63,15 @@ try { } $pageTitle = "Éditer : " . $editTitle; -$extraJs = ["/assets/js/overtype.min.js"]; -$extraJsInline = <<<'JS' + +$initialContent = ''; +$extraJs = []; +$extraJsInline = ''; + +if ($editType === 'page' || $editType === 'about_page') { + $initialContent = $page["content"] ?? ""; + $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'), { @@ -69,12 +81,19 @@ var editor = new OT(document.getElementById('editor'), { onChange: function(value) { hidden.value = value; } }); JS; - -$initialContent = ''; -if ($editType === 'page') { - $initialContent = $page["content"] ?? ""; } elseif ($editType === 'form_help') { $initialContent = $formHelpContent; + $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; } $isAdmin = true; diff --git a/app/public/admin/contenus.php b/app/public/admin/contenus.php index 4543709..d5b139c 100644 --- a/app/public/admin/contenus.php +++ b/app/public/admin/contenus.php @@ -10,9 +10,12 @@ if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } +$allowedPageSlugs = ['about', 'licenses']; + try { $db = new Database(); - $pages = $db->getAllPages(); + $allPages = $db->getAllPages(); + $pages = array_values(array_filter($allPages, fn($p) => in_array($p['slug'], $allowedPageSlugs, true))); $aproposKeys = $db->getAllAproposContents(); $formHelpBlocks = $db->getAllFormHelpBlocks(); } catch (Exception $e) { diff --git a/app/public/assets/css/apropos.css b/app/public/assets/css/apropos.css index d7b312a..f71b938 100644 --- a/app/public/assets/css/apropos.css +++ b/app/public/assets/css/apropos.css @@ -89,6 +89,21 @@ opacity: 0.75; } +.apropos-toc-source { + padding-top: var(--space-xs); +} + +.apropos-toc-source a { + font-size: var(--step--2); + color: var(--accent-primary); + text-decoration: none; + transition: opacity 0.15s; +} + +.apropos-toc-source a:hover { + opacity: 0.75; +} + /* ------------------------------------------------------------------ */ /* Right — main content area */ /* ------------------------------------------------------------------ */ diff --git a/app/src/Controllers/AboutController.php b/app/src/Controllers/AboutController.php index 293379e..b386242 100644 --- a/app/src/Controllers/AboutController.php +++ b/app/src/Controllers/AboutController.php @@ -21,15 +21,12 @@ class AboutController if (empty(trim($rawContent)) || trim($rawContent) === 'Contenu à venir') { $rawContent = $this->defaultContent; } - $contacts = $db->getAproposContent('contacts'); - $credits = $db->getAproposContent('credits'); - $contacts = is_array($contacts) && !empty($contacts) ? $contacts : null; - $credits = is_array($credits) && !empty($credits) ? $credits : null; + $contacts = $db->getAproposContent('contacts'); + $contacts = is_array($contacts) && !empty($contacts) ? $contacts : null; } catch (Exception $e) { error_log('Error loading about page: ' . $e->getMessage()); $rawContent = $this->defaultContent; $contacts = null; - $credits = null; } $pd = new Parsedown(); @@ -39,7 +36,6 @@ class AboutController 'currentNav' => 'apropos', 'aboutHtml' => $pd->text($rawContent), 'contacts' => $contacts, - 'credits' => $credits, 'pageTitle' => 'À Propos – XAMXAM', 'metaDescription' => "À propos de XAMXAM, le répertoire des mémoires de fin d'études de l'erg – École de Recherches Graphiques de Bruxelles.", 'extraCss' => ['/assets/css/apropos.css'], diff --git a/app/src/Database.php b/app/src/Database.php index 666644f..646c85e 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2239,31 +2239,21 @@ class Database return null; } - $value = $row['value']; - if ($key === 'erg_url') { - return $value; - } - $decoded = json_decode($value, true); + $decoded = json_decode($row['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 + * Save an apropos content value by key (contacts JSON). */ - public function saveAproposContent(string $key, $value): void + public function saveAproposContent(string $key, array $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; + $storedValue = json_encode($value, JSON_UNESCAPED_UNICODE); $stmt = $this->pdo->prepare( - 'UPDATE apropos_contents SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?' + 'INSERT INTO apropos_contents (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP' ); - $stmt->execute([$storedValue, $key]); + $stmt->execute([$key, $storedValue]); } /** diff --git a/app/storage/logs/admin.log b/app/storage/logs/admin.log index 7bbdadb..4938927 100644 --- a/app/storage/logs/admin.log +++ b/app/storage/logs/admin.log @@ -11,3 +11,4 @@ {"timestamp":"2026-05-05T16:57:57+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":15,"ids":[53,52,51,50,49,48,47,46,45,44,43,42,41,40,39]}} {"timestamp":"2026-05-05T16:58:02+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"thesis","action":"publish","status":"success","context":{"count":25,"ids":[178,177,176,175,174,173,172,171,170,169,168,167,166,165,164,163,162,161,160,159,158,157,156,155,154]}} {"timestamp":"2026-05-07T16:15:27+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"system","action":"files_export","status":"success","context":{"file_count":0,"byte_size":248}} +{"timestamp":"2026-05-07T16:56:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0","resource":"apropos","action":"edit","status":"success","context":{"key":"credits"}} diff --git a/app/storage/schema.sql b/app/storage/schema.sql index 5c60784..2ff8ad0 100644 --- a/app/storage/schema.sql +++ b/app/storage/schema.sql @@ -408,8 +408,8 @@ SELECT * FROM smtp_settings WHERE id = 1; 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 + key TEXT NOT NULL UNIQUE, -- 'contacts' + value TEXT, -- JSON array updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -433,23 +433,6 @@ INSERT OR IGNORE INTO apropos_contents (key, value) VALUES {"text": "Brigitte Ledune", "url": "", "email": "brigitte.ledune@erg.be"} ] } -]'), - ('credits', '[ - { - "label": "Design & développement", - "entries": [ - {"text": "Olivia Marly", "url": ""}, - {"text": "Théophile Gerveau-Mercie", "url": ""}, - {"text": "Théo Hennequin", "url": ""} - ] - }, - { - "label": "Typographies", - "entries": [ - {"text": "Ductus (Amélie Dumont)", "url": ""}, - {"text": "BBB DM Sans", "url": ""} - ] - } ]'); -- ============================================================================ diff --git a/app/templates/admin/apropos-groups-form.php b/app/templates/admin/apropos-groups-form.php new file mode 100644 index 0000000..f137dd6 --- /dev/null +++ b/app/templates/admin/apropos-groups-form.php @@ -0,0 +1,115 @@ + +
+ + + + $group): ?> +
+ Contact + + + + + $entry): ?> +
+ + + + + + +
+ + + +
+ + + + + + + + + +
+ + diff --git a/app/templates/admin/contenus-edit.php b/app/templates/admin/contenus-edit.php index ee3d6b0..c6fad73 100644 --- a/app/templates/admin/contenus-edit.php +++ b/app/templates/admin/contenus-edit.php @@ -1,7 +1,34 @@

Éditer :

- + + + +

Contenu de la page

+
+ "> + + + + +
+ + +
+ + +

Contacts

+ + +
"> @@ -38,125 +65,6 @@ - - "> - - - $group): ?> -
- - - - - - - - - - - $entry): ?> -
- - - - - - - - -
- - - -
- - - - - - - - - -
- - +
diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php index faebe64..bf4281b 100644 --- a/app/templates/admin/contenus.php +++ b/app/templates/admin/contenus.php @@ -37,38 +37,6 @@ -

À propos

- - - - - - - - - - - - - 'Contacts', - 'credits' => 'Crédits', - }; - ?> - - - - - - - - -
CléTypeMis à jourAction
- Éditer -
- diff --git a/app/templates/public/about.php b/app/templates/public/about.php index 81a61b3..73ff735 100644 --- a/app/templates/public/about.php +++ b/app/templates/public/about.php @@ -33,15 +33,18 @@ function renderEntries(array $entries): string {
  • Contacts
  • -
  • Crédits
  • -
    Site de l'erg ↗
    +
    + + Code source ↗ + +
    @@ -77,20 +80,27 @@ function renderEntries(array $entries): string { - - +

    Crédits

    -
    -
    -
    +
    Design & développement
    +
    + Olivia Marly, + Théophile Gerveau-Mercie & + Théo Hennequin +
    +
    +
    +
    Typographies
    +
    + Ductus (Amélie Dumont) & + BBB DM Sans +
    -
    -