diff --git a/TODO.md b/TODO.md index d56b38d..260513d 100644 --- a/TODO.md +++ b/TODO.md @@ -5,3 +5,7 @@ - [x] Admin nav-logo: use grid layout with SVG + text horizontally aligned and vertically centered - [x] repertoire.css: .rep-entry → step-1, years col → step-3, col h2 → step-1 - [x] Rework tfe.php layout: row1 author above title, row2 meta+synopsis 2-col grid, row3 flex files +- [x] Contacts: allow empty name OR empty role (not both) when saving a contact +- [x] Sidebar links: make "site de l'erg" and "code source" editable via admin panel +- [x] Admin contact blocks: grid layout (3 columns: Nom, Email, Lien) +- [x] Admin contacts: bouton supprimer un contact avec réindexation automatique diff --git a/app/public/admin/actions/apropos.php b/app/public/admin/actions/apropos.php index 3fa006c..5b00c10 100644 --- a/app/public/admin/actions/apropos.php +++ b/app/public/admin/actions/apropos.php @@ -14,7 +14,9 @@ if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || die("Erreur de sécurité : token invalide."); } -$allowedKeys = ['contacts']; +$urlKeys = ['erg_site_url', 'source_code_url']; +$groupKeys = ['contacts']; +$allowedKeys = array_merge($urlKeys, $groupKeys); $aproposKey = $_POST['apropos_key'] ?? ''; if (!in_array($aproposKey, $allowedKeys)) { die("Clé invalide."); @@ -26,33 +28,45 @@ require_once __DIR__ . '/../../../src/ErrorHandler.php'; try { $db = new Database(); - $groups = $_POST['groups'] ?? []; - $cleaned = []; - foreach ($groups as $group) { - $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 (in_array($aproposKey, $urlKeys)) { + // ── URL-based keys (sidebar links) ── + $url = trim($_POST['url'] ?? ''); + if ($url !== '' && !filter_var($url, FILTER_VALIDATE_URL)) { + die("URL invalide."); } - if (empty($entries)) continue; - $cleaned[] = ['role' => $role, 'entries' => $entries]; + $db->saveAproposContent($aproposKey, $url); + } else { + // ── Group-based keys (contacts) ── + $groups = $_POST['groups'] ?? []; + $cleaned = []; + + foreach ($groups as $group) { + $role = trim($group['role'] ?? ''); + $entries = []; + foreach ($group['entries'] ?? [] as $entry) { + $text = trim($entry['text'] ?? ''); + if ($text === '') continue; + $e = [ + 'text' => $text, + 'email' => trim($entry['email'] ?? ''), + ]; + $urlEntry = trim($entry['url'] ?? ''); + if ($urlEntry !== '') $e['url'] = $urlEntry; + $entries[] = $e; + } + // Keep group if it has a role OR at least one entry + if ($role === '' && empty($entries)) continue; + $cleaned[] = ['role' => $role, 'entries' => $entries]; + } + + if (empty($cleaned)) { + die("Au moins un groupe avec des entrées est requis."); + } + + $db->saveAproposContent($aproposKey, $cleaned); } - if (empty($cleaned)) { - die("Au moins un groupe avec des entrées est requis."); - } - - $db->saveAproposContent($aproposKey, $cleaned); AdminLogger::make()->logAproposEdit($aproposKey); App::flash('success', "Contenu « $aproposKey » mis à jour avec succès."); } catch (Exception $e) { diff --git a/app/public/admin/contenus-edit.php b/app/public/admin/contenus-edit.php index f980b2b..efac644 100644 --- a/app/public/admin/contenus-edit.php +++ b/app/public/admin/contenus-edit.php @@ -10,7 +10,7 @@ if (empty($_SESSION["csrf_token"])) { } $allowedPageSlugs = ["about", "licenses"]; -$allowedApropos = ["contacts"]; +$allowedApropos = ["contacts", "erg_site_url", "source_code_url"]; $pageSlug = $_GET["slug"] ?? ""; $aproposKey = $_GET["apropos"] ?? ""; @@ -44,6 +44,8 @@ try { $editType = 'about_page'; $aboutContacts = $db->getAproposContent('contacts'); $aboutContacts = is_array($aboutContacts) ? $aboutContacts : []; + $ergSiteUrl = $db->getAproposContent('erg_site_url'); + $sourceCodeUrl = $db->getAproposContent('source_code_url'); } else { $editType = "page"; } diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 5f34d77..7860d52 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -2116,3 +2116,27 @@ th.admin-ap-col { .recap-dl dd:last-of-type { margin-bottom: 0; } + +/* ── Apropos contacts grid ──────────────────────────────────────────────── */ + +.apropos-entry { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--space-s); + margin-bottom: var(--space-xs); +} + +.apropos-entry > div { + display: flex; + flex-direction: column; +} + +.apropos-entry label { + font-size: var(--step--1); + margin-bottom: var(--space-3xs); +} + +.apropos-entry input { + width: 100%; + box-sizing: border-box; +} diff --git a/app/src/Controllers/AboutController.php b/app/src/Controllers/AboutController.php index 0e36106..cdb8cf2 100644 --- a/app/src/Controllers/AboutController.php +++ b/app/src/Controllers/AboutController.php @@ -26,10 +26,14 @@ class AboutController } $contacts = $db->getAproposContent('contacts'); $contacts = is_array($contacts) && !empty($contacts) ? $contacts : null; + $ergSiteUrl = $db->getAproposContent('erg_site_url'); + $sourceCodeUrl = $db->getAproposContent('source_code_url'); } catch (Exception $e) { ErrorHandler::log('about_page', $e); $rawContent = $this->defaultContent; $contacts = null; + $ergSiteUrl = null; + $sourceCodeUrl = null; } $converter = new CommonMarkConverter(['html_input' => 'strip']); @@ -38,6 +42,8 @@ class AboutController 'currentNav' => 'apropos', 'aboutHtml' => EmailObfuscator::obfuscateHtml($converter->convert($rawContent)->getContent()), 'contacts' => $contacts, + 'ergSiteUrl' => $ergSiteUrl, + 'sourceCodeUrl' => $sourceCodeUrl, '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 962aa56..033307c 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2732,8 +2732,8 @@ class Database /** * 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 + * @param string $key 'contacts', 'credits', 'erg_site_url', 'source_code_url' + * @return array|string|null JSON-decoded value (array for contacts, string for URLs) */ public function getAproposContent(string $key) { @@ -2745,13 +2745,28 @@ class Database } $decoded = json_decode($row['value'], true); - return is_array($decoded) ? $decoded : null; + if (is_array($decoded)) { + return $decoded; + } + if (is_string($decoded)) { + return $decoded; + } + // Legacy: raw URL strings stored before JSON encoding was enforced + if (is_string($row['value']) && $row['value'] !== '') { + $trimmed = trim($row['value']); + if (filter_var($trimmed, FILTER_VALIDATE_URL)) { + return $trimmed; + } + } + return null; } /** - * Save an apropos content value by key (contacts JSON). + * Save an apropos content value by key. + * @param string $key + * @param array|string $value Array for structured data (contacts), string for URLs */ - public function saveAproposContent(string $key, array $value): void + public function saveAproposContent(string $key, $value): void { $storedValue = json_encode($value, JSON_UNESCAPED_UNICODE); $stmt = $this->pdo->prepare( diff --git a/app/storage/logs/admin-2026-06-08.log b/app/storage/logs/admin-2026-06-08.log index 1990f51..474d7dd 100644 --- a/app/storage/logs/admin-2026-06-08.log +++ b/app/storage/logs/admin-2026-06-08.log @@ -2,3 +2,5 @@ {"timestamp":"2026-06-08T09:24:44+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"settings","action":"formulaire_update","status":"success","context":{"values":{"restricted_files_enabled":"0"}}} {"timestamp":"2026-06-08T10:04:25+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"thesis","action":"edit","status":"success","context":{"thesis_id":26,"title":"DepNum"}} {"timestamp":"2026-06-08T10:04:50+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"thesis","action":"publish","status":"success","context":{"count":1,"ids":[26]}} +{"timestamp":"2026-06-08T15:09:14+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"apropos","action":"edit","status":"success","context":{"key":"contacts"}} +{"timestamp":"2026-06-08T15:09:46+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0","resource":"apropos","action":"edit","status":"success","context":{"key":"contacts"}} diff --git a/app/templates/admin/apropos-groups-form.php b/app/templates/admin/apropos-groups-form.php index f137dd6..ba52fda 100644 --- a/app/templates/admin/apropos-groups-form.php +++ b/app/templates/admin/apropos-groups-form.php @@ -21,24 +21,34 @@ $entry): ?>
- - - - - - +
+ + +
+
+ + +
+
+ + +
+ @@ -52,12 +62,18 @@ @@ -69,6 +85,10 @@ + @@ -81,7 +101,36 @@ var entryTpl = document.getElementById('entry-template-f-' + key).innerHTML; var groupTpl = document.getElementById('group-template-f-' + key).innerHTML; - form.querySelectorAll('.add-entry-btn-f').forEach(function(btn) { + function reindexGroups() { + var fieldsets = form.querySelectorAll('fieldset.apropos-group'); + groupCount = fieldsets.length; + fieldsets.forEach(function(fs, i) { + var newIdx = i; + var legend = fs.querySelector('legend'); + if (legend) legend.textContent = 'Contact ' + (newIdx + 1); + + // Update name attributes on all inputs + fs.querySelectorAll('input').forEach(function(inp) { + if (inp.name) { + inp.name = inp.name.replace(/groups\[\d+\]/, 'groups[' + newIdx + ']'); + } + if (inp.id) { + inp.id = inp.id.replace(/(group_f_contacts_|entry_f_contacts_)\d+/, '$1' + newIdx); + } + }); + + // Update for attributes on labels + fs.querySelectorAll('label[for]').forEach(function(lbl) { + lbl.setAttribute('for', lbl.getAttribute('for').replace(/(group_f_contacts_|entry_f_contacts_)\d+/, '$1' + newIdx)); + }); + + // Update data-group on add-entry button + var addBtn = fs.querySelector('.add-entry-btn-f'); + if (addBtn) addBtn.dataset.group = newIdx; + }); + } + + function bindAddEntry(btn) { btn.addEventListener('click', function() { var gi = parseInt(this.dataset.group); var fieldset = this.closest('fieldset'); @@ -89,7 +138,21 @@ var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount); this.insertAdjacentHTML('beforebegin', html); }); - }); + } + + function bindDeleteGroup(btn) { + btn.addEventListener('click', function() { + var fieldset = this.closest('fieldset'); + if (fieldset) { + fieldset.remove(); + reindexGroups(); + } + }); + } + + // Bind existing buttons + form.querySelectorAll('.add-entry-btn-f').forEach(bindAddEntry); + form.querySelectorAll('.delete-group-btn-f').forEach(bindDeleteGroup); form.querySelector('.add-group-btn-f').addEventListener('click', function() { groupCount++; @@ -98,17 +161,13 @@ var newGroup = this.previousElementSibling; if (newGroup && newGroup.classList.contains('apropos-group')) { - var btn = newGroup.querySelector('.add-entry-btn-f'); - if (btn) { - btn.dataset.group = groupCount; - btn.addEventListener('click', function() { - var gi = parseInt(this.dataset.group); - var fieldset = this.closest('fieldset'); - var entryCount = fieldset.querySelectorAll('.apropos-entry').length; - var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount); - this.insertAdjacentHTML('beforebegin', html); - }); + var addBtn = newGroup.querySelector('.add-entry-btn-f'); + if (addBtn) { + addBtn.dataset.group = groupCount; + bindAddEntry(addBtn); } + var delBtn = newGroup.querySelector('.delete-group-btn-f'); + if (delBtn) bindDeleteGroup(delBtn); } }); })(); diff --git a/app/templates/admin/contenus-edit.php b/app/templates/admin/contenus-edit.php index eded90e..e723649 100644 --- a/app/templates/admin/contenus-edit.php +++ b/app/templates/admin/contenus-edit.php @@ -28,6 +28,32 @@ include APP_ROOT . '/templates/admin/apropos-groups-form.php'; ?> + +

Liens de la barre latérale

+
+ + + + + +
+ +
+ + + + + +
+
"> diff --git a/app/templates/public/about.php b/app/templates/public/about.php index 2e5e8df..805cacf 100644 --- a/app/templates/public/about.php +++ b/app/templates/public/about.php @@ -45,12 +45,12 @@ function renderEntries(array $entries): string
  • Crédits
  • - + Site de l'erg ↗
    - + Code source ↗